claude-profile: remove provider-yaml system, embed model config in profiles
Provider selection phase removed; Anthropic driver→model_id is now hardcoded in the script and non-Anthropic profiles supply provider.env (MODEL_ID, ANTHROPIC_BASE_URL, ANTHROPIC_API_KEY_FILE). Test suite baseline changes 58 → 41 assertions: the 17 dropped assertions covered the removed provider-yaml selection phase, not retained behaviour. Includes refactor-derived memory: decisions (provider architecture, driver enforcement via --model), bash gotchas (read -rp under set -e, never eval user config), process lessons.
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
# claude-profile — Launch Claude Code with a profile, engagement mode, and provider
|
||||
# claude-profile — Launch Claude Code with a profile and engagement mode
|
||||
#
|
||||
# Usage:
|
||||
# claude-profile # interactive: profile + mode + provider
|
||||
# claude-profile <profile> # profile fixed, rest interactive
|
||||
# claude-profile --mode <name> # mode fixed, profile + provider interactive
|
||||
# claude-profile --provider <name> # provider fixed, profile + mode interactive
|
||||
# claude-profile # interactive: profile + mode
|
||||
# claude-profile <profile> # profile fixed, mode interactive
|
||||
# claude-profile --mode <name> # mode fixed, profile interactive
|
||||
# claude-profile --preset <name> # all from presets.yaml
|
||||
# claude-profile <profile> --mode <name> # profile + mode fixed
|
||||
# claude-profile --dryrun [...] # show resolved values, do not launch
|
||||
@@ -23,7 +22,6 @@ SCRIPT_PATH="$(readlink -f "$0")"
|
||||
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
MODES_DIR="$REPO_ROOT/data/claude-profile/modes"
|
||||
PROVIDERS_DIR="$REPO_ROOT/data/claude-profile/providers"
|
||||
|
||||
# context-load is on PATH (symlinked into ~/sbin from claude-foundations).
|
||||
# Fall back to a sibling location for testing isolation.
|
||||
@@ -33,13 +31,13 @@ else
|
||||
CONTEXT_LOADER="${SCRIPT_DIR}/context-load"
|
||||
fi
|
||||
|
||||
# Driver logical name -> default provider (fallback when --provider not given)
|
||||
driver_default_provider() {
|
||||
# Driver logical name -> Anthropic model ID
|
||||
driver_model_id() {
|
||||
case "$1" in
|
||||
haiku) echo "anthropic-haiku" ;;
|
||||
sonnet) echo "anthropic-sonnet" ;;
|
||||
opus) echo "anthropic-opus" ;;
|
||||
opus-1m) echo "anthropic-opus-1m" ;;
|
||||
haiku) echo "claude-haiku-4-5-20251001" ;;
|
||||
sonnet) echo "claude-sonnet-4-6" ;;
|
||||
opus) echo "claude-opus-4-6" ;;
|
||||
opus-1m) echo "claude-opus-4-6[1m]" ;;
|
||||
*) err "Unknown driver '$1'. Valid: haiku sonnet opus opus-1m" ;;
|
||||
esac
|
||||
}
|
||||
@@ -48,7 +46,6 @@ driver_default_provider() {
|
||||
|
||||
PROFILE=""
|
||||
MODE=""
|
||||
PROVIDER=""
|
||||
PROJECT=""
|
||||
PRESET=""
|
||||
DRYRUN=0
|
||||
@@ -61,7 +58,6 @@ usage() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--mode) MODE="${2:-}"; shift 2 ;;
|
||||
--provider) PROVIDER="${2:-}"; shift 2 ;;
|
||||
--preset) PRESET="${2:-}"; shift 2 ;;
|
||||
--project) PROJECT="${2:-}"; shift 2 ;;
|
||||
--dryrun|-n) DRYRUN=1; shift ;;
|
||||
@@ -81,8 +77,8 @@ while [[ $# -gt 0 ]]; do
|
||||
done
|
||||
|
||||
# Mutual exclusion
|
||||
if [[ -n "$PRESET" && ( -n "$MODE" || -n "$PROJECT" || -n "$PROVIDER" ) ]]; then
|
||||
echo "Error: --preset is mutually exclusive with --mode, --provider, and --project" >&2
|
||||
if [[ -n "$PRESET" && ( -n "$MODE" || -n "$PROJECT" ) ]]; then
|
||||
echo "Error: --preset is mutually exclusive with --mode and --project" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -145,40 +141,6 @@ read_preset_block() {
|
||||
' "$file"
|
||||
}
|
||||
|
||||
# Extract a scalar field from a provider YAML file (non-indented key: value).
|
||||
parse_provider_field() {
|
||||
local file=$1 key=$2
|
||||
awk -v key="$key" '
|
||||
/^extra_env:[[:space:]]*$/ { in_extra=1; next }
|
||||
/^[a-zA-Z_]/ { in_extra=0 }
|
||||
!in_extra {
|
||||
if (match($0, "^"key":[[:space:]]*")) {
|
||||
val = substr($0, RLENGTH+1)
|
||||
gsub(/^[[:space:]]*"?/, "", val)
|
||||
gsub(/"?[[:space:]]*$/, "", val)
|
||||
print val; exit
|
||||
}
|
||||
}
|
||||
' "$file"
|
||||
}
|
||||
|
||||
# Output KEY=value lines for the extra_env block in a provider YAML file.
|
||||
parse_extra_env() {
|
||||
local file=$1
|
||||
awk '
|
||||
/^extra_env:[[:space:]]*$/ { in_extra=1; next }
|
||||
/^[a-zA-Z_]/ { in_extra=0 }
|
||||
in_extra && /^[[:space:]]+[A-Z_]/ {
|
||||
line = $0; sub(/^[[:space:]]+/, "", line)
|
||||
key = line; val = line
|
||||
sub(/:.*$/, "", key)
|
||||
sub(/^[^:]+:[[:space:]]*/, "", val)
|
||||
gsub(/^[[:space:]]*"?/, "", val); gsub(/"?[[:space:]]*$/, "", val)
|
||||
print key "=" val
|
||||
}
|
||||
' "$file"
|
||||
}
|
||||
|
||||
# List available modes (sorted), printing a numbered menu.
|
||||
# Returns the array of mode names via the global MODE_LIST.
|
||||
MODE_LIST=()
|
||||
@@ -196,23 +158,6 @@ list_modes() {
|
||||
done
|
||||
}
|
||||
|
||||
# List available providers, printing a numbered menu.
|
||||
# Returns the array of provider names via the global PROVIDER_LIST.
|
||||
PROVIDER_LIST=()
|
||||
list_providers() {
|
||||
PROVIDER_LIST=()
|
||||
local i=1
|
||||
for f in "$PROVIDERS_DIR"/*.yaml; do
|
||||
[[ -f "$f" ]] || continue
|
||||
local name display
|
||||
name=$(parse_provider_field "$f" name)
|
||||
display=$(parse_provider_field "$f" display)
|
||||
printf " %d) %-22s — %s\n" "$i" "$name" "$display"
|
||||
PROVIDER_LIST+=("$name")
|
||||
i=$((i + 1))
|
||||
done
|
||||
}
|
||||
|
||||
resolve_profile_dir() {
|
||||
local name=$1
|
||||
if [[ "$name" == "default" ]]; then
|
||||
@@ -235,7 +180,6 @@ reset_wezterm_profile() {
|
||||
|
||||
# Sanity check on the modes directory before doing any picker work.
|
||||
[[ -d "$MODES_DIR" ]] || err "Mode files directory not found at $MODES_DIR"
|
||||
[[ -d "$PROVIDERS_DIR" ]] || err "Providers directory not found at $PROVIDERS_DIR"
|
||||
|
||||
if [[ -n "$PRESET" ]]; then
|
||||
# Find the preset across all profile presets.yaml files.
|
||||
@@ -259,9 +203,7 @@ if [[ -n "$PRESET" ]]; then
|
||||
case "$k" in
|
||||
PRESET_PROFILE) preset_profile_name="$v" ;;
|
||||
PRESET_MODE) MODE="$v" ;;
|
||||
PRESET_PROVIDER) PROVIDER="$v" ;;
|
||||
PRESET_PROJECT) PROJECT="$v" ;;
|
||||
PRESET_TIME_HORIZON) TIME_HORIZON="$v" ;;
|
||||
PRESET_ASYNC) ASYNC_OVERRIDE="$v" ;;
|
||||
PRESET_AUTOLOOP) AUTOLOOP_OVERRIDE="$v" ;;
|
||||
esac
|
||||
@@ -307,6 +249,16 @@ fi
|
||||
|
||||
[[ -d "$target" ]] || err "Profile directory not found: $target"
|
||||
|
||||
# Apply WezTerm theme immediately so the terminal changes during the mode picker.
|
||||
# Read theme name from profile's wezterm-theme file, falling back to the profile name.
|
||||
if [[ -f "$target/wezterm-theme" ]]; then
|
||||
WEZTERM_THEME=$(cat "$target/wezterm-theme")
|
||||
else
|
||||
WEZTERM_THEME="$PROFILE"
|
||||
fi
|
||||
trap reset_wezterm_profile EXIT
|
||||
set_wezterm_profile "$WEZTERM_THEME"
|
||||
|
||||
# === Phase 2: Mode selection ===
|
||||
|
||||
if [[ -z "$MODE" ]]; then
|
||||
@@ -360,22 +312,6 @@ for field_name in DRIVER TAG ASYNC_OK; do
|
||||
fi
|
||||
done
|
||||
|
||||
# Mode-specific interactive questions (skipped if preset)
|
||||
if [[ -z "$PRESET" ]]; then
|
||||
case "$MODE" in
|
||||
deep|hybrid|orch)
|
||||
if [[ -z "${TIME_HORIZON:-}" ]]; then
|
||||
if [[ -t 0 ]]; then
|
||||
read -rp "Time horizon (minutes/hours/overnight) [hours]: " th
|
||||
else
|
||||
th=""
|
||||
fi
|
||||
TIME_HORIZON="${th:-hours}"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Apply preset overrides for async/autoloop
|
||||
[[ -n "${ASYNC_OVERRIDE:-}" ]] && ASYNC_OK="$ASYNC_OVERRIDE"
|
||||
[[ -n "${AUTOLOOP_OVERRIDE:-}" ]] && {
|
||||
@@ -384,55 +320,25 @@ fi
|
||||
fi
|
||||
}
|
||||
|
||||
# === Phase 2.5: Provider selection ===
|
||||
# === Phase 2.5: Resolve model ID ===
|
||||
|
||||
default_provider=$(driver_default_provider "$DRIVER")
|
||||
# Default: Anthropic model from driver
|
||||
MODEL_ID=$(driver_model_id "$DRIVER")
|
||||
|
||||
if [[ -z "$PROVIDER" ]]; then
|
||||
if [[ -z "$PRESET" ]]; then
|
||||
echo ""
|
||||
echo "Provider (model + API):"
|
||||
list_providers
|
||||
echo ""
|
||||
if [[ -t 0 ]]; then
|
||||
read -rp "Choose provider [$default_provider]: " prov_choice
|
||||
else
|
||||
prov_choice=""
|
||||
fi
|
||||
prov_choice="${prov_choice:-$default_provider}"
|
||||
|
||||
if [[ "$prov_choice" =~ ^[0-9]+$ ]]; then
|
||||
if (( prov_choice < 1 || prov_choice > ${#PROVIDER_LIST[@]} )); then
|
||||
err "Invalid provider choice: $prov_choice"
|
||||
fi
|
||||
PROVIDER="${PROVIDER_LIST[$((prov_choice - 1))]}"
|
||||
else
|
||||
PROVIDER="$prov_choice"
|
||||
fi
|
||||
else
|
||||
PROVIDER="$default_provider"
|
||||
fi
|
||||
fi
|
||||
|
||||
provider_file="$PROVIDERS_DIR/$PROVIDER.yaml"
|
||||
[[ -f "$provider_file" ]] || err "Provider not found: $PROVIDER (looked in $PROVIDERS_DIR)"
|
||||
|
||||
PROVIDER_DISPLAY=$(parse_provider_field "$provider_file" display)
|
||||
MODEL_ID=$(parse_provider_field "$provider_file" model_id)
|
||||
PROVIDER_BASE_URL=$(parse_provider_field "$provider_file" base_url)
|
||||
PROVIDER_API_KEY_ENV=$(parse_provider_field "$provider_file" api_key_env)
|
||||
PROVIDER_API_KEY_FILE=$(parse_provider_field "$provider_file" api_key_file)
|
||||
|
||||
[[ -n "$MODEL_ID" ]] || err "Provider file $provider_file is missing required field: model_id"
|
||||
|
||||
# Warn when a non-Anthropic provider is selected but the profile has stored OAuth credentials.
|
||||
# Claude Code uses .credentials.json for auth before checking ANTHROPIC_API_KEY, which
|
||||
# routes requests to Anthropic regardless of ANTHROPIC_BASE_URL.
|
||||
if [[ -n "$PROVIDER_BASE_URL" && -f "$target/.credentials.json" ]]; then
|
||||
echo "Warning: profile '$PROFILE' has stored OAuth credentials ($target/.credentials.json)." >&2
|
||||
echo " These take precedence over ANTHROPIC_API_KEY and will route requests to Anthropic," >&2
|
||||
echo " not $PROVIDER. Use a profile without .credentials.json for non-Anthropic providers." >&2
|
||||
echo " See: claude-profile --help" >&2
|
||||
# Override from profile's provider.env if present
|
||||
PROVIDER_ENV="$target/provider.env"
|
||||
if [[ -f "$PROVIDER_ENV" ]]; then
|
||||
# Source provider.env into a subshell-safe set of variables.
|
||||
# We parse key=value lines rather than sourcing directly to avoid side effects.
|
||||
while IFS='=' read -r key value; do
|
||||
[[ -z "$key" || "$key" == \#* ]] && continue
|
||||
case "$key" in
|
||||
MODEL_ID) MODEL_ID="$value" ;;
|
||||
ANTHROPIC_BASE_URL) PROVIDER_BASE_URL="$value" ;;
|
||||
ANTHROPIC_API_KEY_FILE) PROVIDER_API_KEY_FILE="$value" ;;
|
||||
*) PROVIDER_EXTRA_ENV+=("$key=$value") ;;
|
||||
esac
|
||||
done < "$PROVIDER_ENV"
|
||||
fi
|
||||
|
||||
# === Phase 3: Launch ===
|
||||
@@ -445,10 +351,8 @@ active_env=$(cat <<EOF
|
||||
CLAUDE_MODE=$MODE
|
||||
CLAUDE_MODE_TAG=$TAG
|
||||
CLAUDE_DRIVER=$DRIVER
|
||||
CLAUDE_PROVIDER=$PROVIDER
|
||||
CLAUDE_ESCALATES_TO=${ESCALATES_TO:-none}
|
||||
CLAUDE_PROJECT=${PROJECT:-}
|
||||
CLAUDE_TIME_HORIZON=${TIME_HORIZON:-}
|
||||
CLAUDE_ASYNC_OK=$ASYNC_OK
|
||||
CLAUDE_AUTOLOOP=$AUTOLOOP
|
||||
CLAUDE_PLAN_MODE_AUTO=$PLAN_MODE_AUTO
|
||||
@@ -478,12 +382,11 @@ if (( DRYRUN )); then
|
||||
dryrun_log "Mode: $MODE"
|
||||
dryrun_log "Mode file: $mode_file"
|
||||
dryrun_log "Driver: $DRIVER"
|
||||
dryrun_log "Provider: $PROVIDER ($PROVIDER_DISPLAY)"
|
||||
dryrun_log "Model ID: $MODEL_ID"
|
||||
[[ -n "$PROVIDER_BASE_URL" ]] && dryrun_log "Base URL: $PROVIDER_BASE_URL"
|
||||
[[ -n "$PROVIDER_API_KEY_ENV" ]] && dryrun_log "API key env: $PROVIDER_API_KEY_ENV (from $PROVIDER_API_KEY_FILE)"
|
||||
[[ -n "${PROVIDER_BASE_URL:-}" ]] && dryrun_log "Base URL: $PROVIDER_BASE_URL"
|
||||
[[ -n "${PROVIDER_API_KEY_FILE:-}" ]] && dryrun_log "API key file: $PROVIDER_API_KEY_FILE"
|
||||
[[ -f "$PROVIDER_ENV" ]] && dryrun_log "Provider env: $PROVIDER_ENV"
|
||||
dryrun_log "Project: ${PROJECT:-<not set, will be picked by CLAUDE.md>}"
|
||||
dryrun_log "Time horizon: ${TIME_HORIZON:-<not asked in this mode>}"
|
||||
dryrun_log "Async OK: $ASYNC_OK"
|
||||
dryrun_log "Autoloop: $AUTOLOOP"
|
||||
dryrun_log "Escalates to: ${ESCALATES_TO:-none}"
|
||||
@@ -503,32 +406,31 @@ fi
|
||||
|
||||
# === Real launch from here on ===
|
||||
|
||||
# Set up WezTerm theming and reset trap
|
||||
trap reset_wezterm_profile EXIT
|
||||
export CLAUDE_CONFIG_DIR="$target"
|
||||
set_wezterm_profile "$PROFILE"
|
||||
|
||||
# Disable adaptive thinking (existing behaviour)
|
||||
export CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1
|
||||
|
||||
# Export provider env vars for non-Anthropic providers
|
||||
if [[ -n "$PROVIDER_BASE_URL" ]]; then
|
||||
# Ensure proper colour support
|
||||
export COLORTERM=truecolor
|
||||
|
||||
# Export provider env vars from profile's provider.env
|
||||
if [[ -n "${PROVIDER_BASE_URL:-}" ]]; then
|
||||
export ANTHROPIC_BASE_URL="$PROVIDER_BASE_URL"
|
||||
fi
|
||||
if [[ -n "$PROVIDER_API_KEY_ENV" && -n "$PROVIDER_API_KEY_FILE" ]]; then
|
||||
if [[ -n "${PROVIDER_API_KEY_FILE:-}" ]]; then
|
||||
expanded_key_file="${PROVIDER_API_KEY_FILE/#\~/$HOME}"
|
||||
[[ -f "$expanded_key_file" ]] || err "Provider API key file not found: $expanded_key_file"
|
||||
[[ -f "$expanded_key_file" ]] || err "API key file not found: $expanded_key_file"
|
||||
api_key=$(cat "$expanded_key_file")
|
||||
export "${PROVIDER_API_KEY_ENV}=${api_key}"
|
||||
export ANTHROPIC_API_KEY="$api_key"
|
||||
fi
|
||||
# Export extra_env entries from provider file
|
||||
while IFS='=' read -r k v; do
|
||||
[[ -n "$k" ]] && export "${k}=${v}"
|
||||
done < <(parse_extra_env "$provider_file")
|
||||
for entry in "${PROVIDER_EXTRA_ENV[@]+"${PROVIDER_EXTRA_ENV[@]}"}"; do
|
||||
[[ -n "$entry" ]] && export "$entry"
|
||||
done
|
||||
|
||||
# Write state files
|
||||
printf '%s\n' "$active_env" > "$target/active-mode.env"
|
||||
printf '%s\n' "$MODE" > "$target/last-mode"
|
||||
|
||||
echo "Launching: $PROFILE / $MODE / $PROVIDER${PROJECT:+ / $PROJECT}"
|
||||
echo "Launching: $PROFILE / $MODE${PROJECT:+ / $PROJECT}"
|
||||
exec claude --model "$MODEL_ID" --append-system-prompt "$combined_context" "${CLAUDE_ARGS[@]}"
|
||||
|
||||
Reference in New Issue
Block a user