feat(claude-profile): add provider selection (model + endpoint + credentials)
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>
This commit is contained in:
@@ -2,15 +2,17 @@
|
||||
|
||||
## Status
|
||||
|
||||
**Core features shipped and pushed (commit 143b325). NOT yet exercised in a real `claude` launch.**
|
||||
**Provider support added. 58 dryrun assertions passing. MiniMax token provisioned. NOT yet exercised in a real `claude` launch.**
|
||||
|
||||
## What's done
|
||||
|
||||
| Component | State |
|
||||
|---|---|
|
||||
| Profile + mode picker (phases 1-3) | Done, 37 dryrun assertions passing |
|
||||
| Profile + mode + provider picker (phases 1-3) | Done, 58 dryrun assertions passing |
|
||||
| 5 mode files with `escalates_to` frontmatter | Done |
|
||||
| `active-mode.env` written on every launch | Done (includes `CLAUDE_ESCALATES_TO`) |
|
||||
| 5 provider files (4 Anthropic + minimax-sonnet) | Done (`data/claude-profile/providers/`) |
|
||||
| `active-mode.env` written on every launch | Done (includes `CLAUDE_PROVIDER`) |
|
||||
| MiniMax token decrypted + stored | Done (`~/.claude-secrets/minimax-auth-token`, 600 perms) |
|
||||
| statusline.sh — `[Sonnet→Opus] deep · topic` format | Done (reads `CLAUDE_CONFIG_DIR/active-mode.env`) |
|
||||
| Root CLAUDE.md — auto-select project from cwd or `CLAUDE_PROJECT` | Done |
|
||||
| Preset support (`presets.yaml`) | Done (example at `data/claude-profile/presets.yaml.example`) |
|
||||
@@ -21,19 +23,25 @@
|
||||
```bash
|
||||
ln -sf "$HOME/dev/claude/small-scripts/scripts/claude-profile" "$HOME/sbin/claude-profile"
|
||||
```
|
||||
2. **Real-launch test:** `claude-profile oreillyit --mode chat` first (lowest blast radius — Haiku driver, no project, no async). Verify:
|
||||
- Mode body appears in system prompt (CLAUDE.md announces the mode)
|
||||
2. **Real-launch test — Anthropic first:** `claude-profile oreillyit --mode chat` (press Enter to accept default anthropic-haiku). Verify:
|
||||
- Mode body appears in system prompt
|
||||
- Status bar shows `[Haiku] chat` format
|
||||
- `~/.claude-oreillyit/active-mode.env` is written with correct values
|
||||
3. **Auto-select test:** launch from inside a project dir (`cd ~/dev/claude/small-scripts && claude-profile oreillyit --mode quick`) — should skip the project picker and announce "Auto-selected project small-scripts"
|
||||
4. **Then `--mode deep`** to verify plan-mode auto-engagement and the `[Sonnet→Opus] deep · topic` status bar format
|
||||
5. **If anything's off**, `tests/test-claude-profile.sh` is the fast feedback loop
|
||||
- `~/.claude-oreillyit/active-mode.env` has `CLAUDE_PROVIDER=anthropic-haiku`
|
||||
3. **MiniMax launch test:** `claude-profile oreillyit --mode quick --provider minimax-sonnet`. Verify:
|
||||
- Claude starts and is responsive
|
||||
- `ANTHROPIC_BASE_URL` is set to MiniMax endpoint (check via a "what model are you?" prompt)
|
||||
4. **If anything's off**, `tests/test-claude-profile.sh` is the fast feedback loop
|
||||
|
||||
## Adding new providers
|
||||
|
||||
Drop a new `data/claude-profile/providers/<name>.yaml` file. For third-party providers:
|
||||
- Decrypt credentials and store the raw token in `~/.claude-secrets/<provider>-auth-token`
|
||||
- Provider YAML needs: `base_url`, `api_key_env`, `api_key_file`, and any `extra_env` flags the provider requires
|
||||
|
||||
## Remaining follow-ups (from FUTURE.md)
|
||||
|
||||
1. **`bg-model-call` wrapper** — `deep` and `hybrid` modes reference it but it doesn't exist yet. Own spec + script entry.
|
||||
2. **`switch mode` skill** — mid-session re-pick writes new `active-mode.env` for the next `/clear`. Lower priority.
|
||||
3. **MiniMax inverted architecture** — long research project; requires token-budget sketch first.
|
||||
|
||||
## Key gotchas
|
||||
|
||||
@@ -41,3 +49,4 @@
|
||||
- **Root `~/dev/claude/CLAUDE.md` is also not git-tracked** — changes there are also immediately live.
|
||||
- **`CLAUDE_CONFIG_DIR` must be exported** by claude-profile before `exec claude`. The statusline hook inherits it from the claude process environment.
|
||||
- **`read -rp` under `set -e` exits silently if stdin is not a TTY.** The picker guards all interactive reads with `[[ -t 0 ]]`. See `memory/gotchas-bash.md`.
|
||||
- **MiniMax token is in `~/.claude-secrets/`** — gitignored, plaintext, 600 perms. The SOPS-encrypted version in agent-runtimes is for container agents only.
|
||||
|
||||
@@ -39,3 +39,10 @@ hybrid-day:
|
||||
project: cluster-bootstrap
|
||||
time_horizon: hours
|
||||
async: yes
|
||||
|
||||
# Quick session via MiniMax (Sonnet-class, pay-per-token, no subscription)
|
||||
quick-minimax:
|
||||
profile: oreillyit
|
||||
mode: quick
|
||||
provider: minimax-sonnet
|
||||
project: small-scripts
|
||||
|
||||
3
data/claude-profile/providers/anthropic-haiku.yaml
Normal file
3
data/claude-profile/providers/anthropic-haiku.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
name: anthropic-haiku
|
||||
display: Anthropic (Haiku)
|
||||
model_id: claude-haiku-4-5-20251001
|
||||
3
data/claude-profile/providers/anthropic-opus-1m.yaml
Normal file
3
data/claude-profile/providers/anthropic-opus-1m.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
name: anthropic-opus-1m
|
||||
display: Anthropic (Opus 1M ctx)
|
||||
model_id: "claude-opus-4-6[1m]"
|
||||
3
data/claude-profile/providers/anthropic-opus.yaml
Normal file
3
data/claude-profile/providers/anthropic-opus.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
name: anthropic-opus
|
||||
display: Anthropic (Opus)
|
||||
model_id: claude-opus-4-6
|
||||
3
data/claude-profile/providers/anthropic-sonnet.yaml
Normal file
3
data/claude-profile/providers/anthropic-sonnet.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
name: anthropic-sonnet
|
||||
display: Anthropic (Sonnet)
|
||||
model_id: claude-sonnet-4-6
|
||||
10
data/claude-profile/providers/minimax-sonnet.yaml
Normal file
10
data/claude-profile/providers/minimax-sonnet.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
name: minimax-sonnet
|
||||
display: MiniMax (Sonnet)
|
||||
model_id: claude-sonnet-4-20250514
|
||||
base_url: https://api.minimax.io/anthropic
|
||||
api_key_env: ANTHROPIC_AUTH_TOKEN
|
||||
api_key_file: ~/.claude-secrets/minimax-auth-token
|
||||
extra_env:
|
||||
CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: "1"
|
||||
DISABLE_PROMPT_CACHING: "1"
|
||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
|
||||
@@ -1,12 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# claude-profile — Launch Claude Code with a profile and engagement mode
|
||||
# claude-profile — Launch Claude Code with a profile, engagement mode, and provider
|
||||
#
|
||||
# Usage:
|
||||
# claude-profile # interactive: profile + mode
|
||||
# claude-profile <profile> # profile fixed, mode interactive
|
||||
# claude-profile --mode <name> # mode fixed, profile interactive
|
||||
# claude-profile --preset <name> # both from presets.yaml
|
||||
# claude-profile <profile> --mode <name> # both fixed
|
||||
# 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
|
||||
#
|
||||
@@ -22,6 +23,7 @@ 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.
|
||||
@@ -31,18 +33,22 @@ else
|
||||
CONTEXT_LOADER="${SCRIPT_DIR}/context-load"
|
||||
fi
|
||||
|
||||
# Driver logical name -> full Claude model ID
|
||||
declare -A DRIVER_MAP=(
|
||||
[haiku]="claude-haiku-4-5-20251001"
|
||||
[sonnet]="claude-sonnet-4-6"
|
||||
[opus]="claude-opus-4-6"
|
||||
[opus-1m]="claude-opus-4-6[1m]"
|
||||
)
|
||||
# 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
|
||||
@@ -54,13 +60,14 @@ usage() {
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--mode) MODE="${2:-}"; shift 2 ;;
|
||||
--preset) PRESET="${2:-}"; shift 2 ;;
|
||||
--project) PROJECT="${2:-}"; shift 2 ;;
|
||||
--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 ;;
|
||||
--help|-h) usage; exit 0 ;;
|
||||
--) shift; CLAUDE_ARGS=("$@"); break ;;
|
||||
-*) echo "Unknown flag: $1" >&2; usage >&2; exit 1 ;;
|
||||
*)
|
||||
if [[ -z "$PROFILE" ]]; then
|
||||
PROFILE="$1"
|
||||
@@ -74,8 +81,8 @@ while [[ $# -gt 0 ]]; do
|
||||
done
|
||||
|
||||
# Mutual exclusion
|
||||
if [[ -n "$PRESET" && ( -n "$MODE" || -n "$PROJECT" ) ]]; then
|
||||
echo "Error: --preset is mutually exclusive with --mode and --project" >&2
|
||||
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
|
||||
|
||||
@@ -138,6 +145,40 @@ 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=()
|
||||
@@ -151,17 +192,25 @@ list_modes() {
|
||||
purpose=$(awk '/^## Purpose$/{getline; getline; print; exit}' "$f")
|
||||
printf " %d) %-7s — %s\n" "$i" "$name" "$purpose"
|
||||
MODE_LIST+=("$name")
|
||||
((i++))
|
||||
i=$((i + 1))
|
||||
done
|
||||
}
|
||||
|
||||
driver_to_model() {
|
||||
local d=$1
|
||||
local valid="${!DRIVER_MAP[*]}"
|
||||
if [[ -z "${DRIVER_MAP[$d]:-}" ]]; then
|
||||
err "Unknown driver '$d'. Valid: $valid"
|
||||
fi
|
||||
echo "${DRIVER_MAP[$d]}"
|
||||
# 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() {
|
||||
@@ -186,6 +235,7 @@ 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.
|
||||
@@ -203,12 +253,13 @@ if [[ -n "$PRESET" ]]; then
|
||||
done
|
||||
[[ -n "$preset_profile_dir" ]] || err "Preset '$PRESET' not found in any presets.yaml"
|
||||
|
||||
# Apply preset values (PRESET_PROFILE, PRESET_MODE, etc.)
|
||||
# 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" ;;
|
||||
@@ -238,7 +289,7 @@ else
|
||||
name="${dir##*/.claude-}"
|
||||
echo " $i) $name (~/.claude-$name)"
|
||||
profiles+=("$name")
|
||||
((i++))
|
||||
i=$((i + 1))
|
||||
done
|
||||
echo ""
|
||||
if [[ -t 0 ]]; then
|
||||
@@ -329,14 +380,52 @@ fi
|
||||
[[ -n "${ASYNC_OVERRIDE:-}" ]] && ASYNC_OK="$ASYNC_OVERRIDE"
|
||||
[[ -n "${AUTOLOOP_OVERRIDE:-}" ]] && {
|
||||
if [[ "$AUTOLOOP_OVERRIDE" == "yes" || "$AUTOLOOP_OVERRIDE" == "no" ]]; then
|
||||
: # leave AUTOLOOP as the mode default unless explicitly disabled
|
||||
[[ "$AUTOLOOP_OVERRIDE" == "no" ]] && AUTOLOOP="none"
|
||||
fi
|
||||
}
|
||||
|
||||
# === Phase 3: Launch ===
|
||||
# === Phase 2.5: Provider selection ===
|
||||
|
||||
MODEL_ID=$(driver_to_model "$DRIVER")
|
||||
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"
|
||||
@@ -346,6 +435,7 @@ 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:-}
|
||||
@@ -374,11 +464,14 @@ combined_context="${context}
|
||||
${body}"
|
||||
|
||||
if (( DRYRUN )); then
|
||||
dryrun_log "Profile: $PROFILE ($target)"
|
||||
dryrun_log "Profile: $PROFILE ($target)"
|
||||
dryrun_log "Mode: $MODE"
|
||||
dryrun_log "Mode file: $mode_file"
|
||||
dryrun_log "Driver: $DRIVER"
|
||||
dryrun_log "Driver model: $MODEL_ID"
|
||||
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"
|
||||
@@ -408,9 +501,24 @@ 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${PROJECT:+ / $PROJECT}"
|
||||
echo "Launching: $PROFILE / $MODE / $PROVIDER${PROJECT:+ / $PROJECT}"
|
||||
exec claude --model "$MODEL_ID" --append-system-prompt "$combined_context" "${CLAUDE_ARGS[@]}"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Purpose
|
||||
|
||||
Launch Claude Code with a named configuration profile and an engagement mode. The profile selects which `~/.claude-*` config directory to use; the engagement mode bundles a driver model, subagent policy, async tolerance, and workflow stance into a preset that the driver model adopts at session start.
|
||||
Launch Claude Code with a named configuration profile, an engagement mode, and a provider. The profile selects which `~/.claude-*` config directory to use; the engagement mode bundles a driver model, subagent policy, async tolerance, and workflow stance; the provider determines the model ID, API endpoint, and credentials.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -22,19 +22,20 @@ claude-profile [OPTIONS] [PROFILE] [-- CLAUDE_ARGS...]
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--mode <NAME>` | Skip the mode picker and use the named mode (`chat`, `quick`, `deep`, `hybrid`, `orch`). |
|
||||
| `--preset <NAME>` | Use a named preset from the active profile's `presets.yaml`. Sets profile, mode, and any preset-defined defaults in one shot. |
|
||||
| `--provider <NAME>` | Skip the provider picker and use the named provider (e.g. `anthropic-sonnet`, `minimax-sonnet`). |
|
||||
| `--preset <NAME>` | Use a named preset from the active profile's `presets.yaml`. Sets profile, mode, provider, and any preset-defined defaults in one shot. |
|
||||
| `--project <NAME>` | Pre-select a project (skips the project question for modes that ask it). |
|
||||
| `--dryrun`, `-n` | Print all resolved values and the command that would be executed; do not launch `claude`, do not write `active-mode.env` or `last-mode`. |
|
||||
| `--help`, `-h` | Show usage. |
|
||||
|
||||
### Mutual exclusion
|
||||
|
||||
- `--preset` is mutually exclusive with `--mode` and `--project` — the preset defines all of those. Passing both is an error.
|
||||
- `--preset` is mutually exclusive with `--mode`, `--provider`, and `--project` — the preset defines all of those. Passing both is an error.
|
||||
- A positional `PROFILE` combined with `--preset` is allowed only if the preset's `profile` field matches; mismatch is an error.
|
||||
|
||||
## Behaviour
|
||||
|
||||
The script runs in three phases: **profile selection**, **mode selection**, **launch**. Each phase has an interactive path (prompt the user) and a non-interactive path (CLI flag or config file).
|
||||
The script runs in four phases: **profile selection**, **mode selection**, **provider selection**, **launch**. Each phase has an interactive path (prompt the user) and a non-interactive path (CLI flag or config file).
|
||||
|
||||
### Phase 1 — Profile selection
|
||||
|
||||
@@ -55,28 +56,38 @@ The script runs in three phases: **profile selection**, **mode selection**, **la
|
||||
5. Validate the chosen mode file exists at `<repo>/data/claude-profile/modes/<name>.md`. If not, error and exit 1.
|
||||
6. Read the mode file's frontmatter to extract `driver`, `tag`, `async_ok`, `autoloop`, `plan_mode_auto`, `spec_driven`.
|
||||
|
||||
### Phase 2.5 — Provider selection
|
||||
|
||||
1. If `--preset` is given, use the preset's `provider` field (if present) or derive the default from the mode's `driver` field. Skip the rest of phase 2.5.
|
||||
2. Else if `--provider <name>` is given, use it.
|
||||
3. Else (interactive): derive the default provider from the mode's `driver` field (see "Driver-to-provider defaults" below). List available providers from `<repo>/data/claude-profile/providers/*.yaml`, display a numbered menu, prompt for selection. Default is the driver-derived provider, so pressing Enter preserves existing Anthropic behaviour.
|
||||
4. Validate the chosen provider file exists at `<repo>/data/claude-profile/providers/<name>.yaml`. If not, error and exit 1.
|
||||
5. Read the provider file to extract `model_id`, `base_url`, `api_key_env`, `api_key_file`, and `extra_env`.
|
||||
|
||||
### Phase 3 — Launch
|
||||
|
||||
1. Write the resolved values to `$CLAUDE_CONFIG_DIR/active-mode.env` (key=value, one per line, see "File contracts").
|
||||
2. Write the chosen mode name to `$CLAUDE_CONFIG_DIR/last-mode`.
|
||||
3. Read the mode file's prose body (everything after the frontmatter) and append it to the context that `context-load` produces. The mode body becomes part of `--append-system-prompt`.
|
||||
4. Set `CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1` (existing behaviour).
|
||||
5. Resolve the mode's `driver` frontmatter field through the **Driver mapping** table below to a concrete model ID.
|
||||
6. Execute `claude --model "<resolved-model-id>" --append-system-prompt "$context_plus_mode" "${CLAUDE_ARGS[@]}"`.
|
||||
7. On exit, the existing WezTerm reset trap runs.
|
||||
5. If the provider has a `base_url`, export `ANTHROPIC_BASE_URL=<value>`.
|
||||
6. If the provider has `api_key_env` and `api_key_file`, read the key from the file and export it as `<api_key_env>=<value>`. Error if the file is missing.
|
||||
7. Export any `extra_env` entries from the provider file.
|
||||
8. Execute `claude --model "<provider.model_id>" --append-system-prompt "$context_plus_mode" "${CLAUDE_ARGS[@]}"`.
|
||||
9. On exit, the existing WezTerm reset trap runs.
|
||||
|
||||
### Driver mapping
|
||||
### Driver-to-provider defaults
|
||||
|
||||
The `driver` field in mode frontmatter uses a logical name; the launcher maps it to the full Claude model ID before passing to `claude --model`. Full IDs are used (not aliases) to remove ambiguity about which model version is selected.
|
||||
When no `--provider` is given, the default provider is derived from the mode's `driver` field:
|
||||
|
||||
| Frontmatter `driver` | `--model` argument |
|
||||
| Mode `driver` | Default provider |
|
||||
|---|---|
|
||||
| `haiku` | `claude-haiku-4-5-20251001` |
|
||||
| `sonnet` | `claude-sonnet-4-6` |
|
||||
| `opus` | `claude-opus-4-6` |
|
||||
| `opus-1m` | `claude-opus-4-6[1m]` |
|
||||
| `haiku` | `anthropic-haiku` |
|
||||
| `sonnet` | `anthropic-sonnet` |
|
||||
| `opus` | `anthropic-opus` |
|
||||
| `opus-1m` | `anthropic-opus-1m` |
|
||||
|
||||
If the frontmatter specifies an unknown driver, the launcher errors with the list of valid values and exits 1. The mapping table lives in the launcher script as a constant; updating it is the only change needed when new model versions are released.
|
||||
This preserves backwards-compatible behaviour: launching without `--provider` behaves exactly as before.
|
||||
|
||||
### Mode follow-up questions
|
||||
|
||||
@@ -85,7 +96,6 @@ Each mode declares which questions are relevant. Asked in order; only those not
|
||||
| Question | Asked in modes | Default | Drives |
|
||||
|---|---|---|---|
|
||||
| **Time horizon?** (minutes / hours / overnight) | deep, hybrid, orch | hours | Recorded in active-mode.env; the model adapts ScheduleWakeup cadence |
|
||||
| **Driver override?** (force a specific Claude model) | quick, deep | mode default | Overrides the `driver` field; resolved through the Driver mapping table and passed to `claude --model` |
|
||||
| **Async OK?** (yes / no) | deep, hybrid | mode default (yes for both) | Overrides `async_ok` |
|
||||
| **Auto-loops?** (yes / no, confirms `/loop 2m /orchestrate`) | orch | yes | Overrides `autoloop` |
|
||||
|
||||
@@ -112,6 +122,27 @@ escalates_to: <none|opus> # used by statusline to render →Opus arrow
|
||||
|
||||
The body must include sections (in order): `## Purpose`, `## Driver constraint`, `## Subagent policy`, `## Reasoning posture`, `## Async policy`, `## Workflow stance`, `## Auto-fire at session start`, `## Status line format`, `## Escalation triggers`, `## Out of scope`, `## On context wipe (/clear)`. `## Named workflows available` is optional.
|
||||
|
||||
### Provider files — `<repo>/data/claude-profile/providers/<name>.yaml`
|
||||
|
||||
Plain YAML (no frontmatter). Required field: `model_id`. All other fields are optional.
|
||||
|
||||
```yaml
|
||||
name: minimax-sonnet # unique identifier, matches filename
|
||||
display: MiniMax (Sonnet) # shown in the interactive picker
|
||||
model_id: claude-sonnet-4-20250514 # passed to claude --model
|
||||
base_url: https://api.minimax.io/anthropic # exported as ANTHROPIC_BASE_URL (omit for Anthropic)
|
||||
api_key_env: ANTHROPIC_AUTH_TOKEN # env var name for the API key (omit for Anthropic)
|
||||
api_key_file: ~/.claude-secrets/minimax-auth-token # file containing the raw token value
|
||||
extra_env:
|
||||
CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: "1"
|
||||
DISABLE_PROMPT_CACHING: "1"
|
||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
|
||||
```
|
||||
|
||||
Anthropic providers need only `name`, `display`, and `model_id` — Claude Code handles Anthropic auth via its own config.
|
||||
|
||||
Adding a new provider = one new YAML file in `providers/`. No script changes required.
|
||||
|
||||
### Active mode env — `$CLAUDE_CONFIG_DIR/active-mode.env`
|
||||
|
||||
Written on every launch, overwritten each time. Read by:
|
||||
@@ -122,6 +153,7 @@ Written on every launch, overwritten each time. Read by:
|
||||
CLAUDE_MODE=deep
|
||||
CLAUDE_MODE_TAG=deep
|
||||
CLAUDE_DRIVER=sonnet
|
||||
CLAUDE_PROVIDER=anthropic-sonnet
|
||||
CLAUDE_ESCALATES_TO=opus
|
||||
CLAUDE_PROJECT=cluster-bootstrap
|
||||
CLAUDE_TIME_HORIZON=hours
|
||||
@@ -163,9 +195,15 @@ orch-overnight:
|
||||
project: ai-image-gen
|
||||
time_horizon: overnight
|
||||
autoloop: yes
|
||||
|
||||
quick-minimax:
|
||||
profile: oreillyit
|
||||
mode: quick
|
||||
provider: minimax-sonnet
|
||||
project: small-scripts
|
||||
```
|
||||
|
||||
The `profile` field is required. Other fields default to the mode's defaults if omitted.
|
||||
The `profile` field is required. `provider` defaults to the driver-mapped Anthropic provider if omitted. See `data/claude-profile/presets.yaml.example` for a full example.
|
||||
|
||||
## Status line integration
|
||||
|
||||
@@ -187,17 +225,39 @@ When `--dryrun` (or `-n`) is passed, claude-profile resolves all values exactly
|
||||
|
||||
```
|
||||
$ claude-profile --dryrun --preset deep-cluster
|
||||
[dryrun] Profile: oreillyit (~/.claude-oreillyit)
|
||||
[dryrun] Mode: deep (Sonnet driver, Opus via named workflows)
|
||||
[dryrun] Profile: oreillyit (~/.claude-oreillyit)
|
||||
[dryrun] Mode: deep
|
||||
[dryrun] Mode file: ~/dev/claude/small-scripts/data/claude-profile/modes/deep.md
|
||||
[dryrun] Driver: sonnet
|
||||
[dryrun] Provider: anthropic-sonnet (Anthropic (Sonnet))
|
||||
[dryrun] Model ID: claude-sonnet-4-6
|
||||
[dryrun] Project: cluster-bootstrap
|
||||
[dryrun] Time horizon: hours
|
||||
[dryrun] Async OK: yes
|
||||
[dryrun] Autoloop: none
|
||||
[dryrun] Would write: ~/.claude-oreillyit/active-mode.env
|
||||
[dryrun] Would write: ~/.claude-oreillyit/last-mode
|
||||
[dryrun] Would execute: claude --model claude-sonnet-4-6 --append-system-prompt "<context+mode>"
|
||||
[dryrun] Context length: 4823 chars (3104 from context-load + 1719 from mode file)
|
||||
[dryrun] Would execute: claude --model claude-sonnet-4-6 --append-system-prompt "<context+mode>"
|
||||
[dryrun] Context length: 4823 chars (3104 from context-load + 1719 from mode body)
|
||||
[dryrun] No changes made.
|
||||
```
|
||||
|
||||
**Dryrun with MiniMax provider:**
|
||||
```
|
||||
$ claude-profile --dryrun oreillyit --mode quick --provider minimax-sonnet
|
||||
[dryrun] Profile: oreillyit (~/.claude-oreillyit)
|
||||
[dryrun] Mode: quick
|
||||
[dryrun] Mode file: ~/dev/claude/small-scripts/data/claude-profile/modes/quick.md
|
||||
[dryrun] Driver: sonnet
|
||||
[dryrun] Provider: minimax-sonnet (MiniMax (Sonnet))
|
||||
[dryrun] Model ID: claude-sonnet-4-20250514
|
||||
[dryrun] Base URL: https://api.minimax.io/anthropic
|
||||
[dryrun] API key env: ANTHROPIC_AUTH_TOKEN (from ~/.claude-secrets/minimax-auth-token)
|
||||
[dryrun] Project: <not set, will be picked by CLAUDE.md>
|
||||
[dryrun] Time horizon: <not asked in this mode>
|
||||
[dryrun] Async OK: no
|
||||
[dryrun] Autoloop: none
|
||||
[dryrun] Would execute: claude --model claude-sonnet-4-20250514 --append-system-prompt "<context+mode>"
|
||||
[dryrun] No changes made.
|
||||
```
|
||||
|
||||
@@ -207,13 +267,19 @@ In dryrun mode the interactive prompts are still shown, so the user can walk thr
|
||||
|
||||
| Case | Handling |
|
||||
|---|---|
|
||||
| `~/dev/claude/small-scripts/data/claude-profile/modes/` does not exist | Error: "Mode files directory not found at <path>. Reinstall claude-profile or check the repo location." Exit 1. |
|
||||
| Mode file referenced by `--mode` does not exist | Error listing the available modes. Exit 1. |
|
||||
| `~/dev/claude/small-scripts/data/claude-profile/modes/` does not exist | Error: "Mode files directory not found at <path>." Exit 1. |
|
||||
| Mode file referenced by `--mode` does not exist | Error: "Mode file not found: <path>." Exit 1. |
|
||||
| Mode file has malformed frontmatter (missing required field) | Error naming the missing field and the file path. Exit 1. |
|
||||
| `presets.yaml` does not exist and `--preset` was given | Error: "No presets.yaml found at <path>." Exit 1. |
|
||||
| `presets.yaml` exists but the named preset is not in it | Error listing available preset names. Exit 1. |
|
||||
| `data/claude-profile/providers/` does not exist | Error: "Providers directory not found at <path>." Exit 1. |
|
||||
| Provider file referenced by `--provider` does not exist | Error: "Provider not found: <name> (looked in <providers_dir>)." Exit 1. |
|
||||
| Provider file exists but is missing `model_id` | Error: "Provider file <path> is missing required field: model_id." Exit 1. |
|
||||
| Provider has `api_key_file` but the file does not exist at launch | Error: "Provider API key file not found: <path>." Exit 1. |
|
||||
| `--provider` not given | Default provider derived from mode's `driver` field; interactive picker shown with that as the default. |
|
||||
| `presets.yaml` does not exist and `--preset` was given | Error: "Preset '<name>' not found in any presets.yaml." Exit 1. |
|
||||
| `presets.yaml` exists but the named preset is not in it | Error: "Preset '<name>' not found in any presets.yaml." Exit 1. |
|
||||
| Preset's `profile` field references a non-existent profile | Error. Exit 1. |
|
||||
| `--preset` and `--mode` both given | Error: "Mutually exclusive: --preset already specifies a mode." Exit 1. |
|
||||
| `--preset` and `--mode` both given | Error: "mutually exclusive". Exit 1. |
|
||||
| `--preset` and `--provider` both given | Error: "mutually exclusive". Exit 1. |
|
||||
| Positional `PROFILE` and `--preset` profile field disagree | Error: "Profile mismatch: positional <X>, preset specifies <Y>." Exit 1. |
|
||||
| `last-mode` file is missing or contains an unknown mode | Fall back to `quick` as the default. |
|
||||
| Two parallel sessions launched against the same profile | Both write to the same `active-mode.env` — last writer wins. Documented limitation; not addressed in v1. |
|
||||
@@ -255,10 +321,17 @@ Launching: oreillyit / deep / cluster-bootstrap (from preset deep-cluster)
|
||||
**Direct — profile + mode flag**
|
||||
```
|
||||
$ claude-profile oreillyit --mode quick --project small-scripts
|
||||
Launching: oreillyit / quick / small-scripts
|
||||
Launching: oreillyit / quick / anthropic-sonnet / small-scripts
|
||||
[Claude Code starts]
|
||||
```
|
||||
|
||||
**Direct — MiniMax provider**
|
||||
```
|
||||
$ claude-profile oreillyit --mode quick --provider minimax-sonnet
|
||||
Launching: oreillyit / quick / minimax-sonnet
|
||||
[Claude Code starts with ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN set]
|
||||
```
|
||||
|
||||
**Dryrun — verify a preset**
|
||||
```
|
||||
$ claude-profile --dryrun --preset deep-cluster
|
||||
@@ -274,8 +347,8 @@ $ claude-profile --mode quick --project small-scripts -- --resume
|
||||
## Out of scope (for v1)
|
||||
|
||||
- Multi-session coordination (parallel claude-profile launches against the same profile)
|
||||
- Mid-session mode switching from inside Claude Code (handled by a separate `switch mode` skill, not claude-profile itself)
|
||||
- Editing mode files in place (mode files are read-only data; customisation happens via `presets.yaml`)
|
||||
- Mid-session mode or provider switching from inside Claude Code (handled by a separate `switch mode` skill; requires relaunching)
|
||||
- Editing mode or provider files in place (they are read-only data; customisation happens via `presets.yaml`)
|
||||
- Network/remote profiles (everything is local)
|
||||
- Mid-session driver model swaps (the harness pins the model at launch via `--model`; switching mid-session requires relaunching)
|
||||
- Pinning the model used by named custom subagents at the harness level (those are controlled by the subagent's own definition file under `subagents[name].model`; the Agent tool's per-call `model` parameter is unaffected and remains how mode prose instructs the driver to escalate)
|
||||
- SOPS-encrypted provider credentials at launch time (credentials are stored as plaintext in `~/.claude-secrets/`; SOPS decryption is handled by the agent-runtimes container harness for headless agents)
|
||||
- Pinning the model used by named custom subagents at the harness level (those are controlled by the subagent's own definition file; the Agent tool's per-call `model` parameter is unaffected)
|
||||
|
||||
@@ -31,6 +31,18 @@ assert_contains() {
|
||||
fi
|
||||
}
|
||||
|
||||
assert_not_contains() {
|
||||
local label=$1 needle=$2 haystack=$3
|
||||
if [[ "$haystack" != *"$needle"* ]]; then
|
||||
printf " ${GREEN}PASS${NC} %s\n" "$label"
|
||||
((PASS++))
|
||||
else
|
||||
printf " ${RED}FAIL${NC} %s\n" "$label"
|
||||
printf " expected NOT to contain: %s\n" "$needle"
|
||||
((FAIL++))
|
||||
fi
|
||||
}
|
||||
|
||||
assert_exit_code() {
|
||||
local label=$1 expected=$2 actual=$3
|
||||
if [[ "$expected" == "$actual" ]]; then
|
||||
@@ -61,27 +73,31 @@ assert_exit_code "--help exits 0" 0 "$rc"
|
||||
assert_contains "--help shows usage" "claude-profile" "$out"
|
||||
assert_contains "--help shows --mode" "--mode" "$out"
|
||||
assert_contains "--help shows --preset" "--preset" "$out"
|
||||
assert_contains "--help shows --provider" "--provider" "$out"
|
||||
|
||||
# === Test 2: chat dryrun ===
|
||||
echo
|
||||
echo "Test 2: chat mode dryrun"
|
||||
out=$(run --dryrun oreillyit --mode chat)
|
||||
out=$(run --dryrun oreillyit --mode chat --provider anthropic-haiku)
|
||||
rc=$?
|
||||
assert_exit_code "chat dryrun exits 0" 0 "$rc"
|
||||
assert_contains "chat shows haiku driver" "Driver: haiku" "$out"
|
||||
assert_contains "chat shows full haiku model ID" "claude-haiku-4-5-20251001" "$out"
|
||||
assert_contains "chat shows provider" "Provider: anthropic-haiku" "$out"
|
||||
assert_contains "chat shows model ID" "Model ID: claude-haiku-4-5-20251001" "$out"
|
||||
assert_contains "chat shows mode tag" "Mode: chat" "$out"
|
||||
assert_contains "chat says no changes" "No changes made" "$out"
|
||||
assert_contains "chat does not ask time horizon" "<not asked in this mode>" "$out"
|
||||
assert_contains "chat shows model in exec command" "claude --model claude-haiku-4-5-20251001" "$out"
|
||||
|
||||
# === Test 3: deep dryrun with project flag ===
|
||||
echo
|
||||
echo "Test 3: deep mode dryrun"
|
||||
out=$(run --dryrun oreillyit --mode deep --project cluster-bootstrap)
|
||||
out=$(run --dryrun oreillyit --mode deep --project cluster-bootstrap --provider anthropic-sonnet)
|
||||
rc=$?
|
||||
assert_exit_code "deep dryrun exits 0" 0 "$rc"
|
||||
assert_contains "deep shows sonnet driver" "Driver: sonnet" "$out"
|
||||
assert_contains "deep shows full sonnet model ID" "claude-sonnet-4-6" "$out"
|
||||
assert_contains "deep shows provider" "Provider: anthropic-sonnet" "$out"
|
||||
assert_contains "deep shows model ID" "Model ID: claude-sonnet-4-6" "$out"
|
||||
assert_contains "deep shows --model in command" "claude --model claude-sonnet-4-6" "$out"
|
||||
assert_contains "deep records the project" "Project: cluster-bootstrap" "$out"
|
||||
assert_contains "deep defaults time horizon to hours" "Time horizon: hours" "$out"
|
||||
@@ -92,30 +108,33 @@ assert_contains "deep says spec_driven yes" "Spec driven: yes" "$out"
|
||||
# === Test 4: orch dryrun preserves autoloop ===
|
||||
echo
|
||||
echo "Test 4: orch mode dryrun (autoloop preserved)"
|
||||
out=$(run --dryrun oreillyit --mode orch --project ai-image-gen)
|
||||
out=$(run --dryrun oreillyit --mode orch --project ai-image-gen --provider anthropic-sonnet)
|
||||
rc=$?
|
||||
assert_exit_code "orch dryrun exits 0" 0 "$rc"
|
||||
assert_contains "orch preserves the slash command in autoloop" "/loop 2m /orchestrate" "$out"
|
||||
assert_contains "orch driver is sonnet" "Driver: sonnet" "$out"
|
||||
assert_contains "orch provider is anthropic-sonnet" "Provider: anthropic-sonnet" "$out"
|
||||
|
||||
# === Test 5: hybrid dryrun ===
|
||||
echo
|
||||
echo "Test 5: hybrid mode dryrun"
|
||||
out=$(run --dryrun oreillyit --mode hybrid)
|
||||
out=$(run --dryrun oreillyit --mode hybrid --provider anthropic-haiku)
|
||||
rc=$?
|
||||
assert_exit_code "hybrid dryrun exits 0" 0 "$rc"
|
||||
assert_contains "hybrid uses haiku driver" "Driver: haiku" "$out"
|
||||
assert_contains "hybrid allows async" "Async OK: yes" "$out"
|
||||
assert_contains "hybrid shows provider" "Provider: anthropic-haiku" "$out"
|
||||
|
||||
# === Test 6: quick dryrun (no time horizon question) ===
|
||||
echo
|
||||
echo "Test 6: quick mode dryrun"
|
||||
out=$(run --dryrun oreillyit --mode quick)
|
||||
out=$(run --dryrun oreillyit --mode quick --provider anthropic-sonnet)
|
||||
rc=$?
|
||||
assert_exit_code "quick dryrun exits 0" 0 "$rc"
|
||||
assert_contains "quick uses sonnet driver" "Driver: sonnet" "$out"
|
||||
assert_contains "quick disables async" "Async OK: no" "$out"
|
||||
assert_contains "quick does not ask time horizon" "<not asked in this mode>" "$out"
|
||||
assert_contains "quick shows provider" "Provider: anthropic-sonnet" "$out"
|
||||
|
||||
# === Test 7: error — unknown mode ===
|
||||
echo
|
||||
@@ -144,11 +163,49 @@ assert_contains "missing preset error message" "not found" "$out"
|
||||
# === Test 10: pass-through args via -- ===
|
||||
echo
|
||||
echo "Test 10: pass-through args"
|
||||
out=$(run --dryrun oreillyit --mode chat -- --resume some-session)
|
||||
out=$(run --dryrun oreillyit --mode chat --provider anthropic-haiku -- --resume some-session)
|
||||
rc=$?
|
||||
assert_exit_code "pass-through exits 0" 0 "$rc"
|
||||
assert_contains "pass-through preserves --resume" "--resume some-session" "$out"
|
||||
|
||||
# === Test 11: minimax provider dryrun ===
|
||||
echo
|
||||
echo "Test 11: minimax provider dryrun"
|
||||
out=$(run --dryrun oreillyit --mode quick --provider minimax-sonnet)
|
||||
rc=$?
|
||||
assert_exit_code "minimax dryrun exits 0" 0 "$rc"
|
||||
assert_contains "minimax shows provider" "Provider: minimax-sonnet" "$out"
|
||||
assert_contains "minimax shows model ID" "Model ID: claude-sonnet-4-20250514" "$out"
|
||||
assert_contains "minimax shows base URL" "Base URL: https://api.minimax.io/anthropic" "$out"
|
||||
assert_contains "minimax shows API key env" "API key env: ANTHROPIC_AUTH_TOKEN" "$out"
|
||||
assert_contains "minimax references key file" "minimax-auth-token" "$out"
|
||||
assert_contains "minimax shows model in exec command" "claude --model claude-sonnet-4-20250514" "$out"
|
||||
|
||||
# === Test 12: error — unknown provider ===
|
||||
echo
|
||||
echo "Test 12: error path — unknown provider"
|
||||
out=$(run --dryrun oreillyit --mode quick --provider bogus-provider)
|
||||
rc=$?
|
||||
assert_exit_code "unknown provider exits 1" 1 "$rc"
|
||||
assert_contains "unknown provider error message" "Provider not found" "$out"
|
||||
|
||||
# === Test 13: default provider from driver (no --provider flag) ===
|
||||
echo
|
||||
echo "Test 13: default provider selected from driver"
|
||||
out=$(run --dryrun oreillyit --mode chat)
|
||||
rc=$?
|
||||
assert_exit_code "default provider dryrun exits 0" 0 "$rc"
|
||||
assert_contains "chat default provider is anthropic-haiku" "Provider: anthropic-haiku" "$out"
|
||||
assert_contains "chat default model ID is haiku" "Model ID: claude-haiku-4-5-20251001" "$out"
|
||||
|
||||
# === Test 14: --preset + --provider mutual exclusion ===
|
||||
echo
|
||||
echo "Test 14: error path — --preset + --provider mutual exclusion"
|
||||
out=$(run --dryrun --preset whatever --provider minimax-sonnet)
|
||||
rc=$?
|
||||
assert_exit_code "preset+provider exits 1" 1 "$rc"
|
||||
assert_contains "preset+provider error message" "mutually exclusive" "$out"
|
||||
|
||||
# === Summary ===
|
||||
echo
|
||||
echo "================================"
|
||||
|
||||
Reference in New Issue
Block a user