Introduces provider files (`data/claude-profile/providers/*.yaml`) that bundle model ID, API base URL, API key source, and extra env vars. Phase 2.5 (provider selection) added between mode selection and launch, with a default derived from the mode's driver field so existing Anthropic usage is unchanged. Ships 5 providers: anthropic-haiku/sonnet/opus/opus-1m and minimax-sonnet. MiniMax token decrypted from agent-runtimes SOPS env, stored at ~/.claude-secrets/minimax-auth-token. Adding new providers = one YAML file, no script changes. 58 dryrun tests passing (up from 37). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
525 lines
17 KiB
Bash
Executable File
525 lines
17 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# claude-profile — Launch Claude Code with a profile, engagement mode, and provider
|
|
#
|
|
# 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 --preset <name> # all from presets.yaml
|
|
# claude-profile <profile> --mode <name> # profile + mode fixed
|
|
# claude-profile --dryrun [...] # show resolved values, do not launch
|
|
# claude-profile [...] -- <claude-args> # extra args passed through to claude
|
|
#
|
|
# Spec: specs/claude-profile.spec.md
|
|
|
|
set -euo pipefail
|
|
|
|
# === Constants ===
|
|
|
|
DEFAULT_PROFILE_DIR="$HOME/.claude"
|
|
PROFILE_PATTERN_PREFIX="$HOME/.claude-"
|
|
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.
|
|
if command -v context-load >/dev/null 2>&1; then
|
|
CONTEXT_LOADER="$(command -v context-load)"
|
|
else
|
|
CONTEXT_LOADER="${SCRIPT_DIR}/context-load"
|
|
fi
|
|
|
|
# Driver logical name -> default provider (fallback when --provider not given)
|
|
driver_default_provider() {
|
|
case "$1" in
|
|
haiku) echo "anthropic-haiku" ;;
|
|
sonnet) echo "anthropic-sonnet" ;;
|
|
opus) echo "anthropic-opus" ;;
|
|
opus-1m) echo "anthropic-opus-1m" ;;
|
|
*) err "Unknown driver '$1'. Valid: haiku sonnet opus opus-1m" ;;
|
|
esac
|
|
}
|
|
|
|
# === CLI parsing ===
|
|
|
|
PROFILE=""
|
|
MODE=""
|
|
PROVIDER=""
|
|
PROJECT=""
|
|
PRESET=""
|
|
DRYRUN=0
|
|
CLAUDE_ARGS=()
|
|
|
|
usage() {
|
|
sed -n '2,12p' "$0" | sed 's/^# \?//'
|
|
}
|
|
|
|
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 ;;
|
|
--help|-h) usage; exit 0 ;;
|
|
--) shift; CLAUDE_ARGS=("$@"); break ;;
|
|
-*) echo "Unknown flag: $1" >&2; usage >&2; exit 1 ;;
|
|
*)
|
|
if [[ -z "$PROFILE" ]]; then
|
|
PROFILE="$1"
|
|
else
|
|
echo "Unexpected positional argument: $1" >&2
|
|
exit 1
|
|
fi
|
|
shift
|
|
;;
|
|
esac
|
|
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
|
|
exit 1
|
|
fi
|
|
|
|
# === Helpers ===
|
|
|
|
err() { echo "Error: $*" >&2; exit 1; }
|
|
|
|
dryrun_log() { printf '[dryrun] %s\n' "$*"; }
|
|
|
|
# Extract a key from the YAML frontmatter of a markdown file.
|
|
# Frontmatter must be the first block, delimited by `---` lines.
|
|
parse_frontmatter() {
|
|
local file=$1 key=$2
|
|
awk -v key="$key" '
|
|
BEGIN { in_fm=0 }
|
|
NR==1 && /^---$/ { in_fm=1; next }
|
|
in_fm && /^---$/ { exit }
|
|
in_fm {
|
|
if (match($0, "^"key":[[:space:]]*")) {
|
|
value = substr($0, RLENGTH+1)
|
|
gsub(/^"|"$/, "", value)
|
|
print value
|
|
exit
|
|
}
|
|
}
|
|
' "$file"
|
|
}
|
|
|
|
# Extract the body of a mode file (everything after the second `---`).
|
|
mode_body() {
|
|
local file=$1
|
|
awk '
|
|
/^---$/ { fm++; next }
|
|
fm >= 2 { print }
|
|
' "$file"
|
|
}
|
|
|
|
# Read a named preset block from a presets.yaml file.
|
|
# Output: KEY=value lines (one per field), keys uppercased and prefixed PRESET_.
|
|
read_preset_block() {
|
|
local file=$1 name=$2
|
|
awk -v target="$name" '
|
|
/^[a-zA-Z][a-zA-Z0-9_-]*:[[:space:]]*$/ {
|
|
block_name = $0
|
|
sub(/:[[:space:]]*$/, "", block_name)
|
|
in_block = (block_name == target)
|
|
next
|
|
}
|
|
/^[a-zA-Z]/ { in_block = 0 }
|
|
in_block && /^[[:space:]]+[a-zA-Z]/ {
|
|
line = $0
|
|
sub(/^[[:space:]]+/, "", line)
|
|
key = line; value = line
|
|
sub(/:.*$/, "", key)
|
|
sub(/^[^:]+:[[:space:]]*/, "", value)
|
|
gsub(/"/, "", value)
|
|
up = toupper(key)
|
|
print "PRESET_" up "=" value
|
|
}
|
|
' "$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=()
|
|
list_modes() {
|
|
MODE_LIST=()
|
|
local i=1
|
|
for f in "$MODES_DIR"/*.md; do
|
|
[[ -f "$f" ]] || continue
|
|
local name purpose
|
|
name=$(basename "$f" .md)
|
|
purpose=$(awk '/^## Purpose$/{getline; getline; print; exit}' "$f")
|
|
printf " %d) %-7s — %s\n" "$i" "$name" "$purpose"
|
|
MODE_LIST+=("$name")
|
|
i=$((i + 1))
|
|
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
|
|
echo "$DEFAULT_PROFILE_DIR"
|
|
else
|
|
echo "${PROFILE_PATTERN_PREFIX}${name}"
|
|
fi
|
|
}
|
|
|
|
# WezTerm theming (existing behaviour, unchanged)
|
|
set_wezterm_profile() {
|
|
local profile_name="$1"
|
|
printf '\e]1337;SetUserVar=%s=%s\a' CLAUDE_PROFILE "$(printf '%s' "$profile_name" | base64)"
|
|
}
|
|
reset_wezterm_profile() {
|
|
printf '\e]1337;SetUserVar=%s=%s\a' CLAUDE_PROFILE "$(printf '%s' '_reset' | base64)"
|
|
}
|
|
|
|
# === Phase 1: Profile selection ===
|
|
|
|
# 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.
|
|
preset_profile_dir=""
|
|
preset_lines=""
|
|
for candidate in "$DEFAULT_PROFILE_DIR" ${PROFILE_PATTERN_PREFIX}*; do
|
|
[[ -d "$candidate" ]] || continue
|
|
local_presets="$candidate/presets.yaml"
|
|
[[ -f "$local_presets" ]] || continue
|
|
if grep -q "^${PRESET}:[[:space:]]*$" "$local_presets"; then
|
|
preset_profile_dir="$candidate"
|
|
preset_lines=$(read_preset_block "$local_presets" "$PRESET")
|
|
break
|
|
fi
|
|
done
|
|
[[ -n "$preset_profile_dir" ]] || err "Preset '$PRESET' not found in any presets.yaml"
|
|
|
|
# Apply preset values
|
|
while IFS='=' read -r k v; do
|
|
[[ -z "$k" ]] && continue
|
|
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
|
|
done <<< "$preset_lines"
|
|
|
|
[[ -n "${preset_profile_name:-}" ]] || err "Preset '$PRESET' is missing required field: profile"
|
|
|
|
# If user also gave a positional profile, it must match
|
|
if [[ -n "$PROFILE" && "$PROFILE" != "$preset_profile_name" ]]; then
|
|
err "Profile mismatch: positional '$PROFILE', preset specifies '$preset_profile_name'"
|
|
fi
|
|
PROFILE="$preset_profile_name"
|
|
target=$(resolve_profile_dir "$PROFILE")
|
|
elif [[ -n "$PROFILE" ]]; then
|
|
target=$(resolve_profile_dir "$PROFILE")
|
|
else
|
|
# Interactive profile picker
|
|
echo "Claude Code profiles:"
|
|
echo ""
|
|
echo " 1) default ($DEFAULT_PROFILE_DIR)"
|
|
profiles=("default")
|
|
i=2
|
|
for dir in ${PROFILE_PATTERN_PREFIX}*; do
|
|
[[ -d "$dir" ]] || continue
|
|
name="${dir##*/.claude-}"
|
|
echo " $i) $name (~/.claude-$name)"
|
|
profiles+=("$name")
|
|
i=$((i + 1))
|
|
done
|
|
echo ""
|
|
if [[ -t 0 ]]; then
|
|
read -rp "Choose profile [1]: " choice
|
|
else
|
|
choice=""
|
|
fi
|
|
choice="${choice:-1}"
|
|
if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#profiles[@]} )); then
|
|
err "Invalid profile choice: $choice"
|
|
fi
|
|
PROFILE="${profiles[$((choice - 1))]}"
|
|
target=$(resolve_profile_dir "$PROFILE")
|
|
fi
|
|
|
|
[[ -d "$target" ]] || err "Profile directory not found: $target"
|
|
|
|
# === Phase 2: Mode selection ===
|
|
|
|
if [[ -z "$MODE" ]]; then
|
|
# Determine default from last-mode file
|
|
last_mode_file="$target/last-mode"
|
|
default_mode="quick"
|
|
if [[ -f "$last_mode_file" ]]; then
|
|
candidate=$(cat "$last_mode_file")
|
|
if [[ -f "$MODES_DIR/$candidate.md" ]]; then
|
|
default_mode="$candidate"
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
echo "Engagement modes:"
|
|
list_modes
|
|
echo ""
|
|
if [[ -t 0 ]]; then
|
|
read -rp "Choose mode [$default_mode]: " mode_choice
|
|
else
|
|
mode_choice=""
|
|
fi
|
|
mode_choice="${mode_choice:-$default_mode}"
|
|
|
|
if [[ "$mode_choice" =~ ^[0-9]+$ ]]; then
|
|
if (( mode_choice < 1 || mode_choice > ${#MODE_LIST[@]} )); then
|
|
err "Invalid mode choice: $mode_choice"
|
|
fi
|
|
MODE="${MODE_LIST[$((mode_choice - 1))]}"
|
|
else
|
|
MODE="$mode_choice"
|
|
fi
|
|
fi
|
|
|
|
mode_file="$MODES_DIR/$MODE.md"
|
|
[[ -f "$mode_file" ]] || err "Mode file not found: $mode_file"
|
|
|
|
# Parse frontmatter
|
|
DRIVER=$(parse_frontmatter "$mode_file" "driver")
|
|
TAG=$(parse_frontmatter "$mode_file" "tag")
|
|
ASYNC_OK=$(parse_frontmatter "$mode_file" "async_ok")
|
|
AUTOLOOP=$(parse_frontmatter "$mode_file" "autoloop")
|
|
PLAN_MODE_AUTO=$(parse_frontmatter "$mode_file" "plan_mode_auto")
|
|
SPEC_DRIVEN=$(parse_frontmatter "$mode_file" "spec_driven")
|
|
ESCALATES_TO=$(parse_frontmatter "$mode_file" "escalates_to")
|
|
|
|
# Validate required frontmatter fields
|
|
for field_name in DRIVER TAG ASYNC_OK; do
|
|
if [[ -z "${!field_name}" ]]; then
|
|
err "Mode file $mode_file is missing required frontmatter field: $(echo "$field_name" | tr '[:upper:]' '[:lower:]')"
|
|
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:-}" ]] && {
|
|
if [[ "$AUTOLOOP_OVERRIDE" == "yes" || "$AUTOLOOP_OVERRIDE" == "no" ]]; then
|
|
[[ "$AUTOLOOP_OVERRIDE" == "no" ]] && AUTOLOOP="none"
|
|
fi
|
|
}
|
|
|
|
# === Phase 2.5: Provider selection ===
|
|
|
|
default_provider=$(driver_default_provider "$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"
|
|
|
|
# === Phase 3: Launch ===
|
|
|
|
# Validate context-load exists
|
|
[[ -x "$CONTEXT_LOADER" ]] || err "context-load not found at $CONTEXT_LOADER"
|
|
|
|
# Build active-mode.env content
|
|
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
|
|
CLAUDE_SPEC_DRIVEN=$SPEC_DRIVEN
|
|
CLAUDE_MODE_FILE=$mode_file
|
|
EOF
|
|
)
|
|
|
|
# Mode body (everything after the frontmatter)
|
|
body=$(mode_body "$mode_file")
|
|
if [[ -z "$body" ]]; then
|
|
err "Mode file has no body — nothing to load into the system prompt: $mode_file"
|
|
fi
|
|
|
|
# Pre-flight context-load (do this even in dryrun so length numbers are accurate,
|
|
# but suppress side effects — context-load is read-only).
|
|
context=$("$CONTEXT_LOADER")
|
|
|
|
combined_context="${context}
|
|
|
|
# === Engagement Mode: ${MODE} ===
|
|
|
|
${body}"
|
|
|
|
if (( DRYRUN )); then
|
|
dryrun_log "Profile: $PROFILE ($target)"
|
|
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)"
|
|
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}"
|
|
dryrun_log "Plan mode auto: $PLAN_MODE_AUTO"
|
|
dryrun_log "Spec driven: $SPEC_DRIVEN"
|
|
dryrun_log "Would write: $target/active-mode.env"
|
|
dryrun_log "Would write: $target/last-mode"
|
|
if (( ${#CLAUDE_ARGS[@]} > 0 )); then
|
|
dryrun_log "Would execute: claude --model $MODEL_ID --append-system-prompt <context+mode> ${CLAUDE_ARGS[*]}"
|
|
else
|
|
dryrun_log "Would execute: claude --model $MODEL_ID --append-system-prompt <context+mode>"
|
|
fi
|
|
dryrun_log "Context length: ${#combined_context} chars (${#context} from context-load + ${#body} from mode body)"
|
|
dryrun_log "No changes made."
|
|
exit 0
|
|
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
|
|
export ANTHROPIC_BASE_URL="$PROVIDER_BASE_URL"
|
|
fi
|
|
if [[ -n "$PROVIDER_API_KEY_ENV" && -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"
|
|
api_key=$(cat "$expanded_key_file")
|
|
export "${PROVIDER_API_KEY_ENV}=${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")
|
|
|
|
# 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}"
|
|
exec claude --model "$MODEL_ID" --append-system-prompt "$combined_context" "${CLAUDE_ARGS[@]}"
|