gitbasher 5.0.1 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +30 -38
  3. package/dist/gitb +732 -276
  4. package/package.json +1 -1
package/dist/gitb CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  set -o pipefail
3
- GITBASHER_VERSION="5.0.1"
3
+ GITBASHER_VERSION="5.2.0"
4
4
  if [ "$1" == "init" ] || [ "$1" == "i" ]; then
5
5
  git init
6
6
  fi
@@ -839,29 +839,7 @@ function config_source_tag {
839
839
  default) echo -e "${GRAY}(default)${ENDCOLOR}" ;;
840
840
  esac
841
841
  }
842
- function print_configuration {
843
- echo -e "${YELLOW}Current configuration:${ENDCOLOR}"
844
- local user_name=$(get_config_value user.name)
845
- if [ -n "$user_name" ]; then
846
- echo -e "\tuser.name:\t${YELLOW}$user_name${ENDCOLOR} $(config_source_tag user.name)"
847
- else
848
- echo -e "\tuser.name:\t${RED}not set${ENDCOLOR}"
849
- fi
850
- local user_email=$(get_config_value user.email)
851
- if [ -n "$user_email" ]; then
852
- echo -e "\tuser.email:\t${YELLOW}$user_email${ENDCOLOR} $(config_source_tag user.email)"
853
- else
854
- echo -e "\tuser.email:\t${RED}not set${ENDCOLOR}"
855
- fi
856
- echo -e "\tdefault:\t${YELLOW}$main_branch${ENDCOLOR} $(config_source_tag gitbasher.branch)"
857
- echo -e "\tseparator:\t${YELLOW}$sep${ENDCOLOR} $(config_source_tag gitbasher.sep)"
858
- echo -e "\teditor:\t\t${YELLOW}$editor${ENDCOLOR} $(config_source_tag core.editor)"
859
- if [ "$ticket_name" != "" ]; then
860
- echo -e "\tticket:\t\t${YELLOW}$ticket_name${ENDCOLOR} $(config_source_tag gitbasher.ticket)"
861
- fi
862
- if [ "$scopes" != "" ]; then
863
- echo -e "\tscopes:\t\t${YELLOW}$scopes${ENDCOLOR} $(config_source_tag gitbasher.scopes)"
864
- fi
842
+ function print_ai_configuration {
865
843
  local ai_provider=$(get_ai_provider)
866
844
  echo -e "\tAI provider:\t${GREEN}$ai_provider${ENDCOLOR} $(config_source_tag gitbasher.ai-provider)"
867
845
  local ai_base_url=$(get_ai_base_url)
@@ -897,12 +875,48 @@ function print_configuration {
897
875
  fi
898
876
  local ai_model=$(get_ai_model)
899
877
  if [ -n "$ai_model" ]; then
900
- echo -e "\tAI model:\t${GREEN}$ai_model${ENDCOLOR} ${GRAY}(all tasks)${ENDCOLOR} $(config_source_tag gitbasher.ai-model)"
878
+ local model_key="gitbasher.ai-model-${ai_provider}"
879
+ if [ "$(config_source "$model_key")" = "default" ]; then
880
+ model_key="gitbasher.ai-model"
881
+ fi
882
+ echo -e "\tAI model:\t${GREEN}$ai_model${ENDCOLOR} $(config_source_tag "$model_key")"
901
883
  else
902
- echo -e "\tAI models:\t${GREEN}$(get_ai_model_for simple)${ENDCOLOR} ${GRAY}(simple/subject)${ENDCOLOR} $(config_source_tag gitbasher.ai-model-simple)"
903
- echo -e "\t\t\t${GREEN}$(get_ai_model_for full)${ENDCOLOR} ${GRAY}(full)${ENDCOLOR} $(config_source_tag gitbasher.ai-model-full)"
904
- echo -e "\t\t\t${GREEN}$(get_ai_model_for grouping)${ENDCOLOR} ${GRAY}(grouping)${ENDCOLOR} $(config_source_tag gitbasher.ai-model-grouping)"
884
+ echo -e "\tAI model:\t${GREEN}$(get_ai_default_model)${ENDCOLOR} ${GRAY}(default)${ENDCOLOR}"
905
885
  fi
886
+ local other_models=""
887
+ while IFS= read -r prov; do
888
+ [ -z "$prov" ] && continue
889
+ [ "$prov" = "$ai_provider" ] && continue
890
+ other_models="${other_models:+${other_models}, }${prov}"
891
+ done < <(list_providers_with_model)
892
+ if [ -n "$other_models" ]; then
893
+ echo -e "\t\t\t${GRAY}also set for: ${other_models}${ENDCOLOR}"
894
+ fi
895
+ }
896
+ function print_configuration {
897
+ echo -e "${YELLOW}Current configuration:${ENDCOLOR}"
898
+ local user_name=$(get_config_value user.name)
899
+ if [ -n "$user_name" ]; then
900
+ echo -e "\tuser.name:\t${YELLOW}$user_name${ENDCOLOR} $(config_source_tag user.name)"
901
+ else
902
+ echo -e "\tuser.name:\t${RED}not set${ENDCOLOR}"
903
+ fi
904
+ local user_email=$(get_config_value user.email)
905
+ if [ -n "$user_email" ]; then
906
+ echo -e "\tuser.email:\t${YELLOW}$user_email${ENDCOLOR} $(config_source_tag user.email)"
907
+ else
908
+ echo -e "\tuser.email:\t${RED}not set${ENDCOLOR}"
909
+ fi
910
+ echo -e "\tdefault:\t${YELLOW}$main_branch${ENDCOLOR} $(config_source_tag gitbasher.branch)"
911
+ echo -e "\tseparator:\t${YELLOW}$sep${ENDCOLOR} $(config_source_tag gitbasher.sep)"
912
+ echo -e "\teditor:\t\t${YELLOW}$editor${ENDCOLOR} $(config_source_tag core.editor)"
913
+ if [ "$ticket_name" != "" ]; then
914
+ echo -e "\tticket:\t\t${YELLOW}$ticket_name${ENDCOLOR} $(config_source_tag gitbasher.ticket)"
915
+ fi
916
+ if [ "$scopes" != "" ]; then
917
+ echo -e "\tscopes:\t\t${YELLOW}$scopes${ENDCOLOR} $(config_source_tag gitbasher.scopes)"
918
+ fi
919
+ print_ai_configuration
906
920
  local ai_proxy=$(get_ai_proxy)
907
921
  if [ -n "$ai_proxy" ]; then
908
922
  echo -e "\tAI proxy:\t${GREEN}$ai_proxy${ENDCOLOR} $(config_source_tag gitbasher.ai-proxy)"
@@ -1548,6 +1562,9 @@ function set_config_value {
1548
1562
  git config --local "$1" "$2"
1549
1563
  else
1550
1564
  git config --global "$1" "$2"
1565
+ if [ "$GITBASHER_NO_REPO" != "true" ]; then
1566
+ git config --local --unset "$1" 2>/dev/null || true
1567
+ fi
1551
1568
  fi
1552
1569
  printf '%s\n' "$2"
1553
1570
  }
@@ -1561,7 +1578,7 @@ function unset_config_value {
1561
1578
  if [ -n "$global_config" ]; then
1562
1579
  echo
1563
1580
  echo -e "${YELLOW}Global $1 is also configured: ${BLUE}$global_config${ENDCOLOR}"
1564
- echo -e "Do you want to clear it ${YELLOW}globally${ENDCOLOR} for all projects (y/n)?"
1581
+ echo -e "Do you want to clear it ${YELLOW}globally${ENDCOLOR} for all projects (Y/n)?"
1565
1582
  local _unset_key=""
1566
1583
  read_key _unset_key || _unset_key="n"
1567
1584
  if is_yes "$_unset_key"; then
@@ -1693,9 +1710,9 @@ readonly OPENAI_API_URL="https://api.openai.com/v1/chat/completions"
1693
1710
  readonly AI_DEFAULT_OLLAMA_HOST="http://localhost:11434"
1694
1711
  readonly AI_DEFAULT_PROVIDER="openrouter"
1695
1712
  readonly AI_TEMPERATURE="0.3"
1696
- readonly AI_MAX_TOKENS_SUBJECT=256
1697
- readonly AI_MAX_TOKENS_SIMPLE=512
1698
- readonly AI_MAX_TOKENS_FULL=1024
1713
+ readonly AI_MAX_TOKENS_SUBJECT=512
1714
+ readonly AI_MAX_TOKENS_SIMPLE=1024
1715
+ readonly AI_MAX_TOKENS_FULL=2048
1699
1716
  function get_ai_api_key {
1700
1717
  local provider=$(get_ai_provider)
1701
1718
  local provider_upper
@@ -1826,74 +1843,74 @@ function is_valid_model_id {
1826
1843
  }
1827
1844
  function ai_provider_requires_api_key {
1828
1845
  case "$(get_ai_provider)" in
1829
- ollama) return 1 ;;
1830
- *) return 0 ;;
1846
+ ollama|claude) return 1 ;;
1847
+ *) return 0 ;;
1831
1848
  esac
1832
1849
  }
1833
1850
  function get_ai_model {
1851
+ local provider=$(get_ai_provider)
1852
+ local provider_model
1853
+ provider_model=$(get_config_value "gitbasher.ai-model-${provider}" "")
1854
+ if [ -n "$provider_model" ]; then
1855
+ echo "$provider_model"
1856
+ return
1857
+ fi
1834
1858
  get_config_value gitbasher.ai-model ""
1835
1859
  }
1836
1860
  function set_ai_model {
1837
- set_config_value gitbasher.ai-model "$1"
1838
- }
1839
- readonly AI_DEFAULT_MODEL_SIMPLE="google/gemini-3.1-flash-lite-preview"
1840
- readonly AI_DEFAULT_MODEL_SUBJECT="google/gemini-3.1-flash-lite-preview"
1841
- readonly AI_DEFAULT_MODEL_FULL="google/gemini-3-flash-preview"
1842
- readonly AI_DEFAULT_MODEL_GROUPING="anthropic/claude-haiku-4.5"
1843
- readonly AI_DEFAULT_MODEL_SIMPLE_OPENAI="gpt-5.4-nano"
1844
- readonly AI_DEFAULT_MODEL_SUBJECT_OPENAI="gpt-5.4-nano"
1845
- readonly AI_DEFAULT_MODEL_FULL_OPENAI="gpt-5.4-mini"
1846
- readonly AI_DEFAULT_MODEL_GROUPING_OPENAI="gpt-5.4-mini"
1847
- readonly AI_DEFAULT_MODEL_SIMPLE_OLLAMA="qwen3:8b"
1848
- readonly AI_DEFAULT_MODEL_SUBJECT_OLLAMA="qwen3:8b"
1849
- readonly AI_DEFAULT_MODEL_FULL_OLLAMA="qwen3:8b"
1850
- readonly AI_DEFAULT_MODEL_GROUPING_OLLAMA="qwen3:8b"
1851
- function get_ai_model_for {
1852
- local task="$1"
1853
- local task_model
1854
- task_model=$(get_config_value "gitbasher.ai-model-$task" "")
1855
- if [ -n "$task_model" ]; then
1856
- echo "$task_model"
1857
- return
1861
+ local provider=$(get_ai_provider)
1862
+ set_config_value "gitbasher.ai-model-${provider}" "$1" "$2"
1863
+ }
1864
+ function migrate_legacy_ai_model_to {
1865
+ local target_provider="$1"
1866
+ local migrated=""
1867
+ local local_legacy=$(git config --local --get gitbasher.ai-model 2>/dev/null)
1868
+ if [ -n "$local_legacy" ]; then
1869
+ if ! git config --local --get "gitbasher.ai-model-${target_provider}" >/dev/null 2>&1; then
1870
+ git config --local "gitbasher.ai-model-${target_provider}" "$local_legacy"
1871
+ migrated="local"
1872
+ fi
1873
+ git config --local --unset gitbasher.ai-model 2>/dev/null || true
1858
1874
  fi
1859
- local global_model
1860
- global_model=$(get_ai_model)
1861
- if [ -n "$global_model" ]; then
1862
- echo "$global_model"
1863
- return
1875
+ local global_legacy=$(git config --global --get gitbasher.ai-model 2>/dev/null)
1876
+ if [ -n "$global_legacy" ]; then
1877
+ if ! git config --global --get "gitbasher.ai-model-${target_provider}" >/dev/null 2>&1; then
1878
+ git config --global "gitbasher.ai-model-${target_provider}" "$global_legacy"
1879
+ migrated="${migrated:+$migrated, }global"
1880
+ fi
1881
+ git config --global --unset gitbasher.ai-model 2>/dev/null || true
1882
+ fi
1883
+ if [ -n "$migrated" ]; then
1884
+ echo -e "${GRAY}↻ Kept model for provider '${target_provider}' (${migrated})${ENDCOLOR}" >&2
1864
1885
  fi
1886
+ }
1887
+ function list_providers_with_model {
1888
+ {
1889
+ git config --local --get-regexp '^gitbasher\.ai-model-' 2>/dev/null
1890
+ git config --global --get-regexp '^gitbasher\.ai-model-' 2>/dev/null
1891
+ } | awk '{print $1}' | sed 's/^gitbasher\.ai-model-//' \
1892
+ | grep -E '^(openrouter|openai|ollama|claude)$' | sort -u
1893
+ }
1894
+ readonly AI_DEFAULT_MODEL_OPENROUTER="google/gemini-3.5-flash"
1895
+ readonly AI_DEFAULT_MODEL_OPENAI="gpt-5.4-mini"
1896
+ readonly AI_DEFAULT_MODEL_OLLAMA="qwen3:8b"
1897
+ readonly AI_DEFAULT_MODEL_CLAUDE="haiku"
1898
+ function get_ai_default_model {
1865
1899
  case "$(get_ai_provider)" in
1866
- openai)
1867
- case "$task" in
1868
- simple) echo "$AI_DEFAULT_MODEL_SIMPLE_OPENAI" ;;
1869
- subject) echo "$AI_DEFAULT_MODEL_SUBJECT_OPENAI" ;;
1870
- full) echo "$AI_DEFAULT_MODEL_FULL_OPENAI" ;;
1871
- grouping) echo "$AI_DEFAULT_MODEL_GROUPING_OPENAI" ;;
1872
- *) echo "$AI_DEFAULT_MODEL_SIMPLE_OPENAI" ;;
1873
- esac
1874
- ;;
1875
- ollama)
1876
- case "$task" in
1877
- simple) echo "$AI_DEFAULT_MODEL_SIMPLE_OLLAMA" ;;
1878
- subject) echo "$AI_DEFAULT_MODEL_SUBJECT_OLLAMA" ;;
1879
- full) echo "$AI_DEFAULT_MODEL_FULL_OLLAMA" ;;
1880
- grouping) echo "$AI_DEFAULT_MODEL_GROUPING_OLLAMA" ;;
1881
- *) echo "$AI_DEFAULT_MODEL_SIMPLE_OLLAMA" ;;
1882
- esac
1883
- ;;
1884
- *)
1885
- case "$task" in
1886
- simple) echo "$AI_DEFAULT_MODEL_SIMPLE" ;;
1887
- subject) echo "$AI_DEFAULT_MODEL_SUBJECT" ;;
1888
- full) echo "$AI_DEFAULT_MODEL_FULL" ;;
1889
- grouping) echo "$AI_DEFAULT_MODEL_GROUPING" ;;
1890
- *) echo "$AI_DEFAULT_MODEL_SIMPLE" ;;
1891
- esac
1892
- ;;
1900
+ openai) echo "$AI_DEFAULT_MODEL_OPENAI" ;;
1901
+ ollama) echo "$AI_DEFAULT_MODEL_OLLAMA" ;;
1902
+ claude) echo "$AI_DEFAULT_MODEL_CLAUDE" ;;
1903
+ *) echo "$AI_DEFAULT_MODEL_OPENROUTER" ;;
1893
1904
  esac
1894
1905
  }
1895
- function set_ai_model_for {
1896
- set_config_value "gitbasher.ai-model-$1" "$2"
1906
+ function resolve_ai_model {
1907
+ local model
1908
+ model=$(get_ai_model)
1909
+ if [ -n "$model" ]; then
1910
+ echo "$model"
1911
+ return
1912
+ fi
1913
+ get_ai_default_model
1897
1914
  }
1898
1915
  function get_ai_proxy {
1899
1916
  get_config_value gitbasher.ai-proxy ""
@@ -2148,11 +2165,89 @@ function ollama_list_models {
2148
2165
  local base resp names
2149
2166
  base=$(ollama_api_base)
2150
2167
  resp=$(curl -s --max-time 5 "${base}/api/tags" 2>/dev/null)
2151
- [ -z "$resp" ] && return 1
2152
2168
  names=$(printf '%s' "$resp" | _parse_ollama_model_names)
2169
+ if [ -z "$names" ] && command -v ollama >/dev/null 2>&1; then
2170
+ names=$(OLLAMA_HOST="$base" ollama list 2>/dev/null \
2171
+ | LC_ALL=C awk 'NR>1 && $1 != "" {print $1}')
2172
+ fi
2153
2173
  [ -z "$names" ] && return 1
2154
2174
  printf '%s\n' "$names"
2155
2175
  }
2176
+ function secure_curl_get {
2177
+ local proxy_url="$1"
2178
+ local api_key="$2"
2179
+ local url="$3"
2180
+ (
2181
+ { set +x; } 2>/dev/null
2182
+ unset HISTFILE
2183
+ local curl_cmd=(
2184
+ curl -s
2185
+ --connect-timeout 10
2186
+ --max-time 20
2187
+ )
2188
+ [ -n "$proxy_url" ] && curl_cmd+=(--proxy "$proxy_url")
2189
+ curl_cmd+=("$url")
2190
+ if [ -n "$api_key" ]; then
2191
+ local escaped_key="${api_key//\\/\\\\}"
2192
+ escaped_key="${escaped_key//\"/\\\"}"
2193
+ "${curl_cmd[@]}" --config - <<EOF 2>/dev/null
2194
+ header = "Authorization: Bearer ${escaped_key}"
2195
+ EOF
2196
+ else
2197
+ "${curl_cmd[@]}" 2>/dev/null
2198
+ fi
2199
+ )
2200
+ }
2201
+ function _parse_model_ids_json {
2202
+ if command -v jq &>/dev/null; then
2203
+ jq -r '.data[]?.id // empty' 2>/dev/null
2204
+ else
2205
+ LC_ALL=C grep -o '"id"[[:space:]]*:[[:space:]]*"[^"]*"' \
2206
+ | LC_ALL=C sed 's/.*"\([^"]*\)"$/\1/'
2207
+ fi
2208
+ }
2209
+ function openrouter_list_models {
2210
+ local sort_key="${1:-top-weekly}"
2211
+ local models_url resp ids
2212
+ models_url="${OPENROUTER_API_URL/chat\/completions/models}?sort=${sort_key}"
2213
+ resp=$(secure_curl_get "$(get_ai_proxy)" "" "$models_url")
2214
+ [ -z "$resp" ] && return 1
2215
+ ids=$(printf '%s' "$resp" | _parse_model_ids_json)
2216
+ [ -z "$ids" ] && return 1
2217
+ printf '%s\n' "$ids"
2218
+ }
2219
+ function openai_list_models {
2220
+ local api_key resp ids
2221
+ api_key=$(get_ai_api_key)
2222
+ [ -z "$api_key" ] && return 1
2223
+ local models_url="${OPENAI_API_URL/chat\/completions/models}"
2224
+ resp=$(secure_curl_get "$(get_ai_proxy)" "$api_key" "$models_url")
2225
+ [ -z "$resp" ] && return 1
2226
+ if command -v jq &>/dev/null; then
2227
+ ids=$(printf '%s' "$resp" | jq -r '.data // [] | sort_by(-(.created // 0)) | .[].id // empty' 2>/dev/null)
2228
+ else
2229
+ ids=$(printf '%s' "$resp" | _parse_model_ids_json)
2230
+ fi
2231
+ ids=$(printf '%s\n' "$ids" \
2232
+ | LC_ALL=C grep -E '^(gpt-|o[0-9]|chatgpt-)' \
2233
+ | LC_ALL=C grep -vE 'embedding|tts|whisper|audio|realtime|dall-e|image|moderation|transcribe|search|instruct')
2234
+ [ -z "$ids" ] && return 1
2235
+ printf '%s\n' "$ids"
2236
+ }
2237
+ function ai_model_smoke_check {
2238
+ local model out rc
2239
+ model=$(resolve_ai_model)
2240
+ echo -e "${YELLOW}Testing model '${model}' with a live request...${ENDCOLOR}"
2241
+ out=$(call_ai_api "Reply with exactly: OK" "ping" 64 "$model" 2>&1)
2242
+ rc=$?
2243
+ if [ "$rc" -eq 0 ] && [ -n "$out" ]; then
2244
+ echo -e "${GREEN}✓ Model '${model}' responded${ENDCOLOR}"
2245
+ return 0
2246
+ fi
2247
+ echo -e "${RED}✗ Model '${model}' did not respond${ENDCOLOR}" >&2
2248
+ [ -n "$out" ] && printf '%s\n' "$out" | head -4 >&2
2249
+ return 1
2250
+ }
2156
2251
  function ai_smoke_check {
2157
2252
  local provider
2158
2253
  provider=$(get_ai_provider)
@@ -2176,6 +2271,17 @@ function ai_smoke_check {
2176
2271
  echo -e "${GREEN}✓ Ollama reachable at ${base} (${count} model(s))${ENDCOLOR}"
2177
2272
  return 0
2178
2273
  fi
2274
+ if [ "$provider" = "claude" ]; then
2275
+ if ! command -v claude >/dev/null 2>&1; then
2276
+ echo -e "${RED}✗ claude CLI not found on PATH.${ENDCOLOR}" >&2
2277
+ echo -e "${CYAN}💡 Install it with ${BLUE}npm install -g @anthropic-ai/claude-code${ENDCOLOR}${CYAN}, then sign in once.${ENDCOLOR}" >&2
2278
+ return 1
2279
+ fi
2280
+ local claude_version
2281
+ claude_version=$(claude --version 2>/dev/null | head -1)
2282
+ echo -e "${GREEN}✓ claude CLI available${claude_version:+ (}${claude_version}${claude_version:+)}${ENDCOLOR}"
2283
+ return 0
2284
+ fi
2179
2285
  local api_key
2180
2286
  api_key=$(get_ai_api_key)
2181
2287
  if [ -z "$api_key" ]; then
@@ -2230,18 +2336,49 @@ function get_ai_timeout {
2230
2336
  printf '%s\n' "$t"
2231
2337
  return
2232
2338
  fi
2233
- if [ "$(get_ai_provider)" = "ollama" ]; then
2234
- printf '300\n'
2339
+ case "$(get_ai_provider)" in
2340
+ ollama|claude) printf '300\n' ;;
2341
+ *) printf '60\n' ;;
2342
+ esac
2343
+ }
2344
+ function _call_claude_cli {
2345
+ local system_prompt="$1"
2346
+ local user_prompt="$2"
2347
+ local model="$3"
2348
+ local timeout_s output rc
2349
+ timeout_s=$(get_ai_timeout)
2350
+ local -a claude_args=(-p --model "$model" --output-format text
2351
+ --strict-mcp-config --mcp-config '{"mcpServers":{}}'
2352
+ --setting-sources "")
2353
+ if command -v perl >/dev/null 2>&1; then
2354
+ output=$(printf '%s\n\n%s' "$system_prompt" "$user_prompt" \
2355
+ | MAX_THINKING_TOKENS=0 perl -e 'alarm shift; exec @ARGV' "$timeout_s" \
2356
+ claude "${claude_args[@]}" 2>/dev/null)
2357
+ rc=$?
2235
2358
  else
2236
- printf '60\n'
2359
+ output=$(printf '%s\n\n%s' "$system_prompt" "$user_prompt" \
2360
+ | MAX_THINKING_TOKENS=0 claude "${claude_args[@]}" 2>/dev/null)
2361
+ rc=$?
2362
+ fi
2363
+ if [ "$rc" -eq 142 ]; then
2364
+ echo -e "${RED}✗ claude timed out after ${timeout_s}s.${ENDCOLOR}" >&2
2365
+ echo -e "${YELLOW}Raise the limit with:${ENDCOLOR} ${GREEN}git config gitbasher.ai-timeout <seconds>${ENDCOLOR}" >&2
2366
+ return 1
2237
2367
  fi
2368
+ if [ "$rc" -ne 0 ] || [ -z "$output" ]; then
2369
+ echo -e "${RED}✗ claude CLI failed (exit ${rc}).${ENDCOLOR}" >&2
2370
+ echo -e "${CYAN}💡 Check it works: ${BLUE}echo hi | claude -p${ENDCOLOR}${CYAN} — you may need to sign in first.${ENDCOLOR}" >&2
2371
+ return 1
2372
+ fi
2373
+ printf '%s\n' "$output"
2238
2374
  }
2239
2375
  function call_ai_api {
2240
2376
  local system_prompt="$1"
2241
2377
  local user_prompt="$2"
2242
2378
  local max_tokens="${3:-$AI_MAX_TOKENS_FULL}"
2243
- local model="${4:-$(get_ai_model_for simple)}"
2379
+ local model="${4:-$(resolve_ai_model)}"
2244
2380
  local response_format="${5:-}"
2381
+ local reasoning_effort="${6:-}"
2245
2382
  local provider=$(get_ai_provider)
2246
2383
  local api_url=$(get_ai_api_url)
2247
2384
  local api_key=$(get_ai_api_key)
@@ -2253,6 +2390,17 @@ function call_ai_api {
2253
2390
  clear_sensitive_vars
2254
2391
  return 1
2255
2392
  fi
2393
+ if [ "$provider" = "claude" ]; then
2394
+ local claude_out
2395
+ claude_out=$(_call_claude_cli "$system_prompt" "$user_prompt" "$model")
2396
+ if [ $? -ne 0 ] || [ -z "$claude_out" ]; then
2397
+ clear_sensitive_vars
2398
+ return 1
2399
+ fi
2400
+ clear_sensitive_vars
2401
+ echo "$claude_out"
2402
+ return 0
2403
+ fi
2256
2404
  local tokens_field="max_tokens"
2257
2405
  if [ "$provider" = "openai" ]; then
2258
2406
  tokens_field="max_completion_tokens"
@@ -2263,6 +2411,21 @@ function call_ai_api {
2263
2411
  gpt-5*|o[0-9]*) include_temperature="" ;;
2264
2412
  esac
2265
2413
  fi
2414
+ case "$reasoning_effort" in
2415
+ low|medium|high|"") : ;;
2416
+ *) reasoning_effort="" ;; # unknown value → provider floor
2417
+ esac
2418
+ local or_reasoning="" oa_reasoning=""
2419
+ if [ -n "$(get_ai_base_url)" ]; then
2420
+ : # custom endpoint — send nothing
2421
+ elif [ "$provider" = "openrouter" ]; then
2422
+ or_reasoning="${reasoning_effort:-minimal}"
2423
+ elif [ "$provider" = "openai" ]; then
2424
+ case "$model" in
2425
+ gpt-5*chat*|chatgpt-*) : ;;
2426
+ gpt-5*|o[0-9]*) oa_reasoning="${reasoning_effort:-none}" ;;
2427
+ esac
2428
+ fi
2266
2429
  local json_payload
2267
2430
  if command -v jq &>/dev/null; then
2268
2431
  json_payload=$(jq -nc \
@@ -2284,6 +2447,17 @@ function call_ai_api {
2284
2447
  --argjson rf "$response_format" \
2285
2448
  '$base + {response_format: $rf}')
2286
2449
  fi
2450
+ if [ -n "$or_reasoning" ]; then
2451
+ json_payload=$(jq -nc \
2452
+ --argjson base "$json_payload" \
2453
+ --arg effort "$or_reasoning" \
2454
+ '$base + {reasoning: {effort: $effort}}')
2455
+ elif [ -n "$oa_reasoning" ]; then
2456
+ json_payload=$(jq -nc \
2457
+ --argjson base "$json_payload" \
2458
+ --arg effort "$oa_reasoning" \
2459
+ '$base + {reasoning_effort: $effort}')
2460
+ fi
2287
2461
  else
2288
2462
  local system_escaped=$(_json_escape_for_payload "$system_prompt")
2289
2463
  local user_escaped=$(_json_escape_for_payload "$user_prompt")
@@ -2296,7 +2470,13 @@ function call_ai_api {
2296
2470
  if [ -n "$include_temperature" ]; then
2297
2471
  temp_field=",\"temperature\":$AI_TEMPERATURE"
2298
2472
  fi
2299
- json_payload="{\"model\":\"$model_escaped\",\"messages\":[{\"role\":\"system\",\"content\":\"$system_escaped\"},{\"role\":\"user\",\"content\":\"$user_escaped\"}]${temp_field},\"${tokens_field}\":$max_tokens${rf_field}}"
2473
+ local reasoning_field=""
2474
+ if [ -n "$or_reasoning" ]; then
2475
+ reasoning_field=",\"reasoning\":{\"effort\":\"$or_reasoning\"}"
2476
+ elif [ -n "$oa_reasoning" ]; then
2477
+ reasoning_field=",\"reasoning_effort\":\"$oa_reasoning\""
2478
+ fi
2479
+ json_payload="{\"model\":\"$model_escaped\",\"messages\":[{\"role\":\"system\",\"content\":\"$system_escaped\"},{\"role\":\"user\",\"content\":\"$user_escaped\"}]${temp_field},\"${tokens_field}\":$max_tokens${rf_field}${reasoning_field}}"
2300
2480
  fi
2301
2481
  local proxy_url=$(get_ai_proxy)
2302
2482
  local safe_proxy_url=""
@@ -2492,11 +2672,34 @@ function call_ai_api {
2492
2672
  clear_sensitive_vars
2493
2673
  return 1
2494
2674
  fi
2675
+ local finish_reason=""
2676
+ if command -v jq &>/dev/null; then
2677
+ finish_reason=$(echo "$response" | jq -r '.choices[0].finish_reason // empty' 2>/dev/null)
2678
+ else
2679
+ case "$response" in
2680
+ *'"finish_reason":"length"'*|*'"finish_reason": "length"'*) finish_reason="length" ;;
2681
+ esac
2682
+ fi
2683
+ if [ "$finish_reason" = "length" ]; then
2684
+ echo -e "${RED}✗ AI response was cut off by the output-token limit.${ENDCOLOR}" >&2
2685
+ echo -e "${CYAN}💡 The model likely spent the budget on internal reasoning — pick a faster model with ${GREEN}gitb cfg model${ENDCOLOR}${CYAN}, or retry.${ENDCOLOR}" >&2
2686
+ clear_sensitive_vars
2687
+ return 1
2688
+ fi
2495
2689
  clear_sensitive_vars
2496
2690
  echo "$ai_response"
2497
2691
  }
2498
2692
  function check_ai_available {
2499
- if ! command -v curl >/dev/null 2>&1; then
2693
+ local active_provider
2694
+ active_provider=$(get_ai_provider)
2695
+ if [ "$active_provider" = "claude" ]; then
2696
+ if ! command -v claude >/dev/null 2>&1; then
2697
+ echo -e "${RED}✗ claude CLI not found on PATH.${ENDCOLOR}" >&2
2698
+ echo -e "${YELLOW}Install it with:${ENDCOLOR} ${GREEN}npm install -g @anthropic-ai/claude-code${ENDCOLOR}" >&2
2699
+ echo -e "${YELLOW}Or switch provider with:${ENDCOLOR} ${GREEN}gitb cfg provider${ENDCOLOR}" >&2
2700
+ return 1
2701
+ fi
2702
+ elif ! command -v curl >/dev/null 2>&1; then
2500
2703
  echo -e "${RED}curl is required for AI functionality but not installed${ENDCOLOR}" >&2
2501
2704
  return 1
2502
2705
  fi
@@ -2692,13 +2895,13 @@ function generate_ai_commit_message {
2692
2895
  local system_prompt user_prompt
2693
2896
  system_prompt=$(build_ai_commit_system_prompt "$mode" "$commit_prefix")
2694
2897
  user_prompt=$(build_ai_commit_user_prompt "$mode" "$detected_scopes" "$provided_scopes" "$rejected_messages")
2695
- local max_tokens model
2898
+ local max_tokens
2696
2899
  case "$mode" in
2697
- subject) max_tokens="$AI_MAX_TOKENS_SUBJECT"; model=$(get_ai_model_for subject) ;;
2698
- full) max_tokens="$AI_MAX_TOKENS_FULL"; model=$(get_ai_model_for full) ;;
2699
- *) max_tokens="$AI_MAX_TOKENS_SIMPLE"; model=$(get_ai_model_for simple) ;;
2900
+ subject) max_tokens="$AI_MAX_TOKENS_SUBJECT" ;;
2901
+ full) max_tokens="$AI_MAX_TOKENS_FULL" ;;
2902
+ *) max_tokens="$AI_MAX_TOKENS_SIMPLE" ;;
2700
2903
  esac
2701
- call_ai_api "$system_prompt" "$user_prompt" "$max_tokens" "$model"
2904
+ call_ai_api "$system_prompt" "$user_prompt" "$max_tokens"
2702
2905
  }
2703
2906
  function clear_sensitive_vars {
2704
2907
  api_key=""
@@ -2732,7 +2935,7 @@ function set_default_branch {
2732
2935
  echo -e "${GREEN}✓ Set '${branch_name}' as the default gitbasher branch in '${project_name}'${ENDCOLOR}"
2733
2936
  echo
2734
2937
  [ "$GITBASHER_NO_REPO" = "true" ] && exit
2735
- echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (y/n)?"
2938
+ echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (Y/n)?"
2736
2939
  yes_no_choice "\nSet '${branch_name}' globally" "true"
2737
2940
  main_branch=$(set_config_value gitbasher.branch $branch_name "true")
2738
2941
  }
@@ -2770,7 +2973,7 @@ function set_sep {
2770
2973
  echo -e "${GREEN}✓ Set '${sep}' as the branch name separator in '${project_name}'${ENDCOLOR}"
2771
2974
  echo
2772
2975
  [ "$GITBASHER_NO_REPO" = "true" ] && exit
2773
- echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (y/n)?"
2976
+ echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (Y/n)?"
2774
2977
  yes_no_choice "\nSet '${sep}' globally" "true"
2775
2978
  sep=$(set_config_value gitbasher.sep "$new_sep" "true")
2776
2979
  }
@@ -2800,7 +3003,7 @@ function set_editor {
2800
3003
  echo -e "${GREEN}✓ Using editor '$editor' (${which_output})${ENDCOLOR}"
2801
3004
  echo
2802
3005
  [ "$GITBASHER_NO_REPO" = "true" ] && exit
2803
- echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (y/n)?"
3006
+ echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (Y/n)?"
2804
3007
  yes_no_choice "\nSet '${editor}' globally" "true"
2805
3008
  editor=$(set_config_value core.editor "$choice" "true")
2806
3009
  }
@@ -2833,12 +3036,15 @@ function set_ticket {
2833
3036
  echo -e "${GREEN}✓ Set '${ticket_name}' as the ticket prefix in '${project_name}'${ENDCOLOR}"
2834
3037
  echo
2835
3038
  [ "$GITBASHER_NO_REPO" = "true" ] && exit
2836
- echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (y/n)?"
3039
+ echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (Y/n)?"
2837
3040
  yes_no_choice "\nSet '${ticket_name}' globally" "true"
2838
3041
  ticket_name=$(set_config_value gitbasher.ticket "$ticket_name" "true")
2839
3042
  }
2840
3043
  function configure_ai_key {
2841
- if [ -z "$(git config --get gitbasher.ai-provider 2>/dev/null)" ] \
3044
+ local _chained=""
3045
+ [ "$1" = "--chained" ] && _chained="true"
3046
+ if [ -z "$_chained" ] \
3047
+ && [ -z "$(git config --get gitbasher.ai-provider 2>/dev/null)" ] \
2842
3048
  && [ -z "$(git config --global --get gitbasher.ai-provider 2>/dev/null)" ]; then
2843
3049
  configure_ai_provider
2844
3050
  return
@@ -2848,9 +3054,10 @@ function configure_ai_key {
2848
3054
  provider_upper=$(echo "$provider" | tr '[:lower:]' '[:upper:]')
2849
3055
  echo -e "${YELLOW}Enter AI API key for provider '${provider}'${ENDCOLOR}"
2850
3056
  echo
2851
- if [ "$provider" = "ollama" ]; then
3057
+ if ! ai_provider_requires_api_key; then
2852
3058
  echo -e "${YELLOW}Provider '${provider}' does not require an API key.${ENDCOLOR}"
2853
3059
  echo -e "Switch providers with ${GREEN}gitb cfg provider${ENDCOLOR} to set one."
3060
+ if [ -n "$_chained" ]; then return 0; fi
2854
3061
  exit
2855
3062
  fi
2856
3063
  ai_api_key=$(get_ai_api_key)
@@ -2858,6 +3065,24 @@ function configure_ai_key {
2858
3065
  echo -e "${YELLOW}No AI API key is set for '${provider}'.${ENDCOLOR}"
2859
3066
  else
2860
3067
  echo -e "AI API key for '${provider}' is ${GREEN}configured${ENDCOLOR}: ${BLUE}$(mask_api_key "$ai_api_key")${ENDCOLOR}"
3068
+ echo
3069
+ echo -e "Press ${YELLOW}Enter${ENDCOLOR} to keep it, ${YELLOW}r${ENDCOLOR} to replace, or ${YELLOW}0${ENDCOLOR} to remove"
3070
+ local _key_action=""
3071
+ read_key _key_action || { if [ -n "$_chained" ]; then return 130; fi; exit; }
3072
+ if [ "$_key_action" = "0" ]; then
3073
+ unset_ai_api_key
3074
+ echo
3075
+ echo -e "${GREEN}✓ Removed AI API key for '${provider}' from '${project_name}'${ENDCOLOR}"
3076
+ if [ -n "$_chained" ]; then return 0; fi
3077
+ exit
3078
+ fi
3079
+ normalize_key "$_key_action"
3080
+ if [ "$normalized_key" != "r" ]; then
3081
+ echo -e "Keeping the existing key."
3082
+ if [ -n "$_chained" ]; then return 0; fi
3083
+ exit
3084
+ fi
3085
+ echo
2861
3086
  fi
2862
3087
  case "$provider" in
2863
3088
  openai)
@@ -2882,7 +3107,8 @@ function configure_ai_key {
2882
3107
  echo -e " ${GREEN}env var${ENDCOLOR} — recommended; the key never touches disk via gitbasher"
2883
3108
  echo -e " ${BLUE}git config${ENDCOLOR} — convenient, but stored in plaintext under .git/config (or ~/.gitconfig if global)"
2884
3109
  echo
2885
- read -n 1 -p "Use environment variable (recommended)? (y/n) " ai_storage_choice || ai_storage_choice="n"
3110
+ read -n 1 -p "Use environment variable (recommended)? (Y/n) " ai_storage_choice \
3111
+ || { if [ -n "$_chained" ]; then echo; return 130; fi; ai_storage_choice="n"; }
2886
3112
  echo
2887
3113
  if is_yes "$ai_storage_choice"; then
2888
3114
  echo
@@ -2892,6 +3118,7 @@ function configure_ai_key {
2892
3118
  echo -e "${CYAN}gitbasher checks env vars before git config every run, so this takes precedence.${ENDCOLOR}"
2893
3119
  echo -e "Append it now with:"
2894
3120
  echo -e " ${GREEN}echo \"export GITB_AI_API_KEY_${provider_upper}='your-${provider}-key'\" >> $(printf '%q' "$rc_file")${ENDCOLOR}"
3121
+ if [ -n "$_chained" ]; then return 0; fi
2895
3122
  exit
2896
3123
  fi
2897
3124
  echo
@@ -2901,20 +3128,24 @@ function configure_ai_key {
2901
3128
  echo -e "${YELLOW}Input is hidden — type the key and press Enter.${ENDCOLOR}"
2902
3129
  read_silent_input ai_key_input "API Key: "
2903
3130
  if [ "$ai_key_input" == "" ]; then
3131
+ if [ -n "$_chained" ]; then return 0; fi
2904
3132
  exit
2905
3133
  fi
2906
3134
  if [ "$ai_key_input" == "0" ]; then
2907
3135
  unset_ai_api_key
2908
3136
  echo
2909
3137
  echo -e "${GREEN}✓ Removed AI API key for '${provider}' from '${project_name}'${ENDCOLOR}"
3138
+ if [ -n "$_chained" ]; then return 0; fi
2910
3139
  exit
2911
3140
  fi
2912
3141
  echo
2913
3142
  if [[ ! "$ai_key_input" =~ ^[a-zA-Z0-9._-]{20,}$ ]]; then
2914
3143
  echo -e "${YELLOW}⚠ API key format does not look like a valid ${provider} key.${ENDCOLOR}" >&2
2915
- read -n 1 -p "Continue anyway? (y/n) " -s choice || choice="n"
3144
+ read -n 1 -p "Continue anyway? (Y/n) " -s choice \
3145
+ || { if [ -n "$_chained" ]; then echo; return 130; fi; choice="n"; }
2916
3146
  echo
2917
3147
  if ! is_yes "$choice"; then
3148
+ if [ -n "$_chained" ]; then return 1; fi
2918
3149
  exit 1
2919
3150
  fi
2920
3151
  fi
@@ -2923,14 +3154,24 @@ function configure_ai_key {
2923
3154
  echo
2924
3155
  ai_smoke_check || true
2925
3156
  echo
2926
- [ "$GITBASHER_NO_REPO" = "true" ] && exit
2927
- echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (y/n)?"
3157
+ if [ "$GITBASHER_NO_REPO" = "true" ]; then
3158
+ if [ -n "$_chained" ]; then return 0; fi
3159
+ exit
3160
+ fi
3161
+ echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (Y/n)?"
2928
3162
  echo -e "${RED}⚠ Global API keys are stored in plaintext in ~/.gitconfig.${ENDCOLOR}"
2929
3163
  echo -e "${CYAN}💡 For better security, set ${BLUE}GITB_AI_API_KEY_${provider_upper}${CYAN} as an environment variable instead.${ENDCOLOR}"
2930
- yes_no_choice "\nSet AI API key globally" "true"
2931
- ai_api_key=$(set_ai_api_key "$ai_key_input" "true")
3164
+ local _key_global=""
3165
+ read_key _key_global || { if [ -n "$_chained" ]; then return 130; fi; _key_global="n"; }
3166
+ if is_yes "$_key_global"; then
3167
+ echo -e "${YELLOW}Set AI API key globally${ENDCOLOR}"
3168
+ ai_api_key=$(set_ai_api_key "$ai_key_input" "true")
3169
+ fi
3170
+ return 0
2932
3171
  }
2933
3172
  function configure_ai_provider {
3173
+ local _chained=""
3174
+ [ "$1" = "--chained" ] && _chained="true"
2934
3175
  echo -e "${YELLOW}Choose AI Provider${ENDCOLOR}"
2935
3176
  echo
2936
3177
  local current_provider=$(get_ai_provider)
@@ -2941,13 +3182,18 @@ function configure_ai_provider {
2941
3182
  fi
2942
3183
  echo
2943
3184
  echo -e "Options:"
2944
- echo -e " 1. ${BLUE}openrouter${ENDCOLOR} Aggregator with hundreds of models — needs an API key"
2945
- echo -e " 2. ${BLUE}openai${ENDCOLOR} OpenAI direct (GPT-5 family) — needs an API key"
2946
- echo -e " 3. ${BLUE}ollama${ENDCOLOR} Local or remote models (you'll set the host) — no API key required"
3185
+ echo -e " 1. ${BOLD}openrouter${ENDCOLOR} Aggregator with hundreds of models — needs an API key"
3186
+ echo -e " 2. ${BOLD}openai${ENDCOLOR} OpenAI direct (GPT-5 family) — needs an API key"
3187
+ echo -e " 3. ${BOLD}ollama${ENDCOLOR} Local or remote models (you'll set the host) — no API key required"
3188
+ echo -e " 4. ${BOLD}claude${ENDCOLOR} Local Claude Code CLI (claude -p) — uses your Claude account, no API key"
2947
3189
  echo
2948
3190
  echo -e "Press Enter to keep the current provider, or enter 0 to reset to default (${AI_DEFAULT_PROVIDER})"
2949
- read_editable_input choice "Choice (1-3): "
3191
+ if ! read_editable_input choice "Choice (1-4): "; then
3192
+ if [ -n "$_chained" ]; then return 130; fi
3193
+ exit
3194
+ fi
2950
3195
  if [ "$choice" == "" ]; then
3196
+ if [ -n "$_chained" ]; then return 0; fi
2951
3197
  exit
2952
3198
  fi
2953
3199
  if [ "$choice" == "0" ]; then
@@ -2955,6 +3201,7 @@ function configure_ai_provider {
2955
3201
  git config --global --unset gitbasher.ai-provider 2>/dev/null
2956
3202
  echo
2957
3203
  echo -e "${GREEN}✓ Reset provider to default (${AI_DEFAULT_PROVIDER})${ENDCOLOR}"
3204
+ if [ -n "$_chained" ]; then return 0; fi
2958
3205
  exit
2959
3206
  fi
2960
3207
  local new_provider
@@ -2963,14 +3210,20 @@ function configure_ai_provider {
2963
3210
  1) new_provider="openrouter" ;;
2964
3211
  2) new_provider="openai" ;;
2965
3212
  3) new_provider="ollama" ;;
3213
+ 4) new_provider="claude" ;;
2966
3214
  *)
2967
3215
  echo -e "${RED}✗ Invalid choice.${ENDCOLOR}" >&2
3216
+ if [ -n "$_chained" ]; then return 1; fi
2968
3217
  exit 1
2969
3218
  ;;
2970
3219
  esac
2971
3220
  migrate_legacy_ai_api_key_to "$current_provider"
3221
+ migrate_legacy_ai_model_to "$current_provider"
3222
+ local prev_local_provider
3223
+ prev_local_provider=$(git config --local --get gitbasher.ai-provider 2>/dev/null)
2972
3224
  set_ai_provider "$new_provider"
2973
3225
  echo -e "${GREEN}✓ Set AI provider to '${new_provider}' for '${project_name}'${ENDCOLOR}"
3226
+ local local_smoke_failed=""
2974
3227
  case "$new_provider" in
2975
3228
  ollama)
2976
3229
  local current_host
@@ -2987,6 +3240,7 @@ function configure_ai_provider {
2987
3240
  host_input="${host_input%/}"
2988
3241
  if ! validate_proxy_url "$host_input"; then
2989
3242
  echo -e "${RED}✗ '$host_input' does not look like a valid host URL (expected http://host[:port]).${ENDCOLOR}"
3243
+ if [ -n "$_chained" ]; then return 1; fi
2990
3244
  exit 1
2991
3245
  fi
2992
3246
  set_ai_ollama_host "$host_input" >/dev/null
@@ -2994,9 +3248,24 @@ function configure_ai_provider {
2994
3248
  fi
2995
3249
  echo
2996
3250
  echo -e "${CYAN}💡 Make sure the Ollama daemon is running (${BLUE}ollama serve${CYAN}) and a model is pulled (${BLUE}ollama pull qwen3:8b${CYAN}).${ENDCOLOR}"
2997
- echo -e "${CYAN}💡 Pick a model with ${GREEN}gitb cfg model${CYAN}.${ENDCOLOR}"
3251
+ if [ -z "$_chained" ]; then
3252
+ echo -e "${CYAN}💡 Pick a model with ${GREEN}gitb cfg model${CYAN}.${ENDCOLOR}"
3253
+ fi
3254
+ echo
3255
+ ai_smoke_check || local_smoke_failed="true"
3256
+ ;;
3257
+ claude)
2998
3258
  echo
2999
- ai_smoke_check || true
3259
+ if command -v claude >/dev/null 2>&1; then
3260
+ ai_smoke_check || local_smoke_failed="true"
3261
+ else
3262
+ echo -e "${RED}✗ claude CLI not found on PATH.${ENDCOLOR}"
3263
+ echo -e "${CYAN}💡 Install it with ${BLUE}npm install -g @anthropic-ai/claude-code${CYAN}, then sign in once.${ENDCOLOR}"
3264
+ local_smoke_failed="true"
3265
+ fi
3266
+ if [ -z "$_chained" ]; then
3267
+ echo -e "${CYAN}💡 Pick a model with ${GREEN}gitb cfg model${CYAN} (haiku, sonnet, opus).${ENDCOLOR}"
3268
+ fi
3000
3269
  ;;
3001
3270
  *)
3002
3271
  if [ -n "$(get_ai_api_key)" ]; then
@@ -3006,10 +3275,29 @@ function configure_ai_provider {
3006
3275
  ;;
3007
3276
  esac
3008
3277
  echo
3278
+ if [ -n "$local_smoke_failed" ]; then
3279
+ echo -e "Keep '${new_provider}' as the AI provider anyway (y/N)?"
3280
+ local _keep_rc=0
3281
+ confirm_destructive || _keep_rc=$?
3282
+ if [ "$_keep_rc" -ne 0 ]; then
3283
+ if [ -n "$prev_local_provider" ]; then
3284
+ git config --local gitbasher.ai-provider "$prev_local_provider"
3285
+ else
3286
+ git config --local --unset gitbasher.ai-provider 2>/dev/null || true
3287
+ fi
3288
+ echo -e "${YELLOW}Provider unchanged — still '$(get_ai_provider)'.${ENDCOLOR}"
3289
+ if [ -n "$_chained" ]; then
3290
+ if [ "$_keep_rc" -eq 2 ]; then return 130; fi
3291
+ return 3
3292
+ fi
3293
+ exit 1
3294
+ fi
3295
+ echo
3296
+ fi
3009
3297
  if [ "$GITBASHER_NO_REPO" != "true" ]; then
3010
- echo -e "Do you want to set this provider ${YELLOW}globally${ENDCOLOR} for all projects (y/n)?"
3298
+ echo -e "Do you want to set this provider ${YELLOW}globally${ENDCOLOR} for all projects (Y/n)?"
3011
3299
  local _prov_key=""
3012
- read_key _prov_key || _prov_key="n"
3300
+ read_key _prov_key || { if [ -n "$_chained" ]; then return 130; fi; _prov_key="n"; }
3013
3301
  if is_yes "$_prov_key"; then
3014
3302
  echo -e "${YELLOW}Set AI provider globally${ENDCOLOR}"
3015
3303
  set_config_value gitbasher.ai-provider "$new_provider" "true"
@@ -3018,12 +3306,12 @@ function configure_ai_provider {
3018
3306
  fi
3019
3307
  fi
3020
3308
  fi
3021
- if ai_provider_requires_api_key && [ -z "$(get_ai_api_key)" ]; then
3309
+ if [ -z "$_chained" ] && ai_provider_requires_api_key && [ -z "$(get_ai_api_key)" ]; then
3022
3310
  echo
3023
3311
  echo -e "${YELLOW}No API key is set for '${new_provider}'.${ENDCOLOR}"
3024
3312
  echo -e "${CYAN}AI commands will be blocked until a key is configured.${ENDCOLOR}"
3025
3313
  echo
3026
- echo -e "Set one now? (y/n)"
3314
+ echo -e "Set one now? (Y/n)"
3027
3315
  if read -n 1 -s key_choice && is_yes "$key_choice"; then
3028
3316
  echo
3029
3317
  configure_ai_key
@@ -3033,105 +3321,247 @@ function configure_ai_provider {
3033
3321
  fi
3034
3322
  fi
3035
3323
  }
3324
+ function _print_model_menu_entry {
3325
+ local _desc=""
3326
+ [ -n "$4" ] && _desc=" ${GRAY}- ${4}${ENDCOLOR}"
3327
+ if [ "$1" = "$3" ]; then
3328
+ echo -e " ${GREEN}${2}. ${1}${ENDCOLOR} (current)${_desc}"
3329
+ else
3330
+ echo -e " ${2}. ${BOLD}${1}${ENDCOLOR}${_desc}"
3331
+ fi
3332
+ }
3036
3333
  function configure_ai_model {
3334
+ local _chained=""
3335
+ [ "$1" = "--chained" ] && _chained="true"
3037
3336
  echo -e "${YELLOW}Configure AI Model${ENDCOLOR}"
3038
3337
  echo
3039
3338
  current_model=$(get_ai_model)
3040
3339
  if [ -n "$current_model" ]; then
3041
- echo -e "Current global override: ${GREEN}$current_model${ENDCOLOR}"
3340
+ echo -e "Current model for provider '${GREEN}$(get_ai_provider)${ENDCOLOR}': ${GREEN}$current_model${ENDCOLOR}"
3042
3341
  else
3043
- echo -e "No global override set — each task uses its per-task default:"
3044
- echo -e " • simple → ${GREEN}$(get_ai_model_for simple)${ENDCOLOR}"
3045
- echo -e " • subject → ${GREEN}$(get_ai_model_for subject)${ENDCOLOR}"
3046
- echo -e " • full → ${GREEN}$(get_ai_model_for full)${ENDCOLOR}"
3047
- echo -e " • grouping → ${GREEN}$(get_ai_model_for grouping)${ENDCOLOR}"
3342
+ echo -e "Provider '${GREEN}$(get_ai_provider)${ENDCOLOR}' uses its default model: ${GREEN}$(get_ai_default_model)${ENDCOLOR}"
3048
3343
  fi
3049
3344
  echo
3050
- echo -e "Set a single model for all tasks (a global override), or leave empty to keep using the per-task defaults."
3051
- echo -e "Per-task overrides can be set directly with git config:"
3052
- echo -e " ${BLUE}git config gitbasher.ai-model-simple <model_id>${ENDCOLOR} (and -subject / -full / -grouping)"
3053
- echo
3054
- echo -e "Popular models for provider '${GREEN}$(get_ai_provider)${ENDCOLOR}':"
3345
+ local -a model_menu=()
3346
+ local model_catalog=""
3347
+ local _m _i
3348
+ local _resolved_model
3349
+ _resolved_model=$(resolve_ai_model)
3055
3350
  case "$(get_ai_provider)" in
3056
- openai)
3057
- echo -e " • ${BLUE}gpt-5.4-nano${ENDCOLOR} - Cheapest+fastest, ideal for one-line subjects (~\$0.20/\$1.25 per M)"
3058
- echo -e " • ${BLUE}gpt-5.4-mini${ENDCOLOR} - Balanced for full-body prose and strict TSV output (~\$0.75/\$4.50 per M)"
3059
- echo -e " • ${BLUE}gpt-5.4${ENDCOLOR} - Flagship, only worth it for the hardest grouping cases"
3060
- ;;
3061
3351
  ollama)
3062
- echo -e " • ${BLUE}qwen3:8b${ENDCOLOR} - Best small instruction-follower; most stable structured output"
3063
- echo -e " • ${BLUE}llama3.3:8b${ENDCOLOR} - Solid general-purpose alternative"
3064
- echo -e " • ${BLUE}qwen2.5-coder:7b${ENDCOLOR} - Code-focused; good when most diffs are code"
3065
- echo -e " Run ${BLUE}ollama list${ENDCOLOR} to see what you have pulled locally."
3352
+ while IFS= read -r _m; do
3353
+ [ -n "$_m" ] && model_menu+=("$_m")
3354
+ done < <(ollama_list_models)
3355
+ if [ ${#model_menu[@]} -gt 0 ]; then
3356
+ echo -e "Installed models on ${BLUE}$(ollama_api_base)${ENDCOLOR}:"
3357
+ _i=1
3358
+ for _m in "${model_menu[@]}"; do
3359
+ _print_model_menu_entry "$_m" "$_i" "$_resolved_model"
3360
+ _i=$((_i + 1))
3361
+ done
3362
+ else
3363
+ echo -e "${YELLOW}No models found on $(ollama_api_base) — is the daemon running?${ENDCOLOR}"
3364
+ fi
3066
3365
  ;;
3067
- *)
3068
- echo -e " • ${BLUE}openrouter/auto${ENDCOLOR} - Auto-select best available model"
3069
- echo -e " • ${BLUE}google/gemini-3.1-flash-lite-preview${ENDCOLOR} - Fastest, cheapest"
3070
- echo -e " • ${BLUE}google/gemini-3-flash-preview${ENDCOLOR} - Better body prose"
3071
- echo -e " • ${BLUE}anthropic/claude-haiku-4.5${ENDCOLOR} - Strict instruction following"
3072
- echo -e " • ${BLUE}openai/gpt-5.4-mini${ENDCOLOR} - Strong reasoning, low cost"
3366
+ openrouter)
3367
+ model_catalog=$(openrouter_list_models top-weekly 2>/dev/null)
3368
+ if [ -n "$model_catalog" ]; then
3369
+ _i=1
3370
+ echo -e "Most used on OpenRouter this week:"
3371
+ while IFS= read -r _m; do
3372
+ model_menu+=("$_m")
3373
+ _print_model_menu_entry "$_m" "$_i" "$_resolved_model"
3374
+ _i=$((_i + 1))
3375
+ done < <(printf '%s\n' "$model_catalog" | head -8)
3376
+ local _newest
3377
+ _newest=$(openrouter_list_models newest 2>/dev/null | head -5)
3378
+ if [ -n "$_newest" ]; then
3379
+ echo -e "Newest:"
3380
+ while IFS= read -r _m; do
3381
+ [ -z "$_m" ] && continue
3382
+ case " ${model_menu[*]} " in *" $_m "*) continue ;; esac
3383
+ model_menu+=("$_m")
3384
+ _print_model_menu_entry "$_m" "$_i" "$_resolved_model"
3385
+ _i=$((_i + 1))
3386
+ done <<< "$_newest"
3387
+ fi
3388
+ fi
3389
+ ;;
3390
+ openai)
3391
+ model_catalog=$(openai_list_models 2>/dev/null)
3392
+ if [ -n "$model_catalog" ]; then
3393
+ echo -e "Newest chat models on your OpenAI account:"
3394
+ _i=1
3395
+ while IFS= read -r _m; do
3396
+ model_menu+=("$_m")
3397
+ _print_model_menu_entry "$_m" "$_i" "$_resolved_model"
3398
+ _i=$((_i + 1))
3399
+ done < <(printf '%s\n' "$model_catalog" \
3400
+ | LC_ALL=C grep -vE -- '-[0-9]{4}-[0-9]{2}-[0-9]{2}$' | head -12)
3401
+ fi
3073
3402
  ;;
3074
3403
  esac
3404
+ if [ ${#model_menu[@]} -eq 0 ]; then
3405
+ local -a _menu_desc=()
3406
+ case "$(get_ai_provider)" in
3407
+ openai)
3408
+ echo -e "Popular OpenAI models:"
3409
+ model_menu=("gpt-5.4-mini" "gpt-5.4-nano" "gpt-5.5")
3410
+ _menu_desc=("Default: as fast as nano with better quality (~\$0.75/\$4.50 per M)"
3411
+ "Budget option for high volume (~\$0.20/\$1.25 per M)"
3412
+ "Flagship, only worth it for the hardest grouping cases")
3413
+ ;;
3414
+ ollama)
3415
+ echo -e "Suggested models (pull first: ${BLUE}ollama pull <id>${ENDCOLOR}):"
3416
+ model_menu=("qwen3:8b" "llama3.3:8b" "qwen2.5-coder:7b")
3417
+ _menu_desc=("Default: best small instruction-follower; most stable structured output"
3418
+ "Solid general-purpose alternative"
3419
+ "Code-focused; good when most diffs are code")
3420
+ ;;
3421
+ claude)
3422
+ echo -e "Model aliases (resolve to the current generation; full ids like ${BLUE}claude-haiku-4-5${ENDCOLOR} also work):"
3423
+ model_menu=("haiku" "sonnet" "opus")
3424
+ _menu_desc=("Default: fastest and cheapest"
3425
+ "Better prose, noticeably slower"
3426
+ "Strongest, for the hardest cases")
3427
+ ;;
3428
+ *)
3429
+ echo -e "Popular OpenRouter models:"
3430
+ model_menu=("google/gemini-3.5-flash" "google/gemini-3.1-flash-lite" "openrouter/auto" "anthropic/claude-haiku-4.5" "anthropic/claude-sonnet-5")
3431
+ _menu_desc=("Default: current flash generation, fast with good prose"
3432
+ "Fastest, cheapest (~\$0.25/\$1.50 per M)"
3433
+ "Auto-select best available model"
3434
+ "Strict instruction following"
3435
+ "Premium quality for the hardest cases")
3436
+ ;;
3437
+ esac
3438
+ _i=0
3439
+ for _m in "${model_menu[@]}"; do
3440
+ _print_model_menu_entry "$_m" "$((_i + 1))" "$_resolved_model" "${_menu_desc[$_i]}"
3441
+ _i=$((_i + 1))
3442
+ done
3443
+ fi
3444
+ echo -e "Enter a number to select, or type a model id."
3075
3445
  echo
3076
- local -a ollama_model_list=()
3077
- if [ "$(get_ai_provider)" = "ollama" ]; then
3078
- local _m _i
3079
- while IFS= read -r _m; do
3080
- [ -n "$_m" ] && ollama_model_list+=("$_m")
3081
- done < <(ollama_list_models)
3082
- if [ ${#ollama_model_list[@]} -gt 0 ]; then
3083
- echo -e "Installed models on ${BLUE}$(ollama_api_base)${ENDCOLOR}:"
3084
- _i=1
3085
- for _m in "${ollama_model_list[@]}"; do
3086
- if [ "$_m" = "$current_model" ]; then
3087
- echo -e " ${GREEN}${_i}. ${_m}${ENDCOLOR} (current)"
3088
- else
3089
- echo -e " ${BLUE}${_i}.${ENDCOLOR} ${_m}"
3090
- fi
3091
- _i=$((_i + 1))
3092
- done
3093
- echo -e "Enter a number to select, or type a model id."
3094
- else
3095
- echo -e "${YELLOW}No models found on $(ollama_api_base) — is the daemon running?${ENDCOLOR}"
3096
- echo -e "Pull one with ${BLUE}ollama pull qwen3:8b${ENDCOLOR}, or type a model id below."
3097
- fi
3098
- echo
3446
+ echo -e "Press Enter to keep the current model, or 0 to reset to the default (${GREEN}$(get_ai_default_model)${ENDCOLOR})"
3447
+ if ! read_editable_input model_input "Model ID: "; then
3448
+ if [ -n "$_chained" ]; then return 130; fi
3449
+ exit
3099
3450
  fi
3100
- echo -e "Press Enter to exit without changes, or 0 to clear the override (use per-task defaults)"
3101
- read_editable_input model_input "Model ID: "
3102
3451
  if [ "$model_input" == "" ]; then
3452
+ if [ -n "$_chained" ]; then return 0; fi
3103
3453
  exit
3104
3454
  fi
3105
3455
  if [ "$model_input" == "0" ]; then
3456
+ git config --unset "gitbasher.ai-model-$(get_ai_provider)" 2>/dev/null
3457
+ git config --global --unset "gitbasher.ai-model-$(get_ai_provider)" 2>/dev/null
3106
3458
  git config --unset gitbasher.ai-model 2>/dev/null
3107
3459
  git config --global --unset gitbasher.ai-model 2>/dev/null
3108
3460
  echo
3109
- echo -e "${GREEN}✓ Cleared global model override — each task now uses its per-task default${ENDCOLOR}"
3461
+ echo -e "${GREEN}✓ Cleared model override — provider '$(get_ai_provider)' now uses its default ($(get_ai_default_model))${ENDCOLOR}"
3462
+ if [ -n "$_chained" ]; then return 0; fi
3110
3463
  exit
3111
3464
  fi
3112
3465
  echo
3113
- if [ ${#ollama_model_list[@]} -gt 0 ] && [[ "$model_input" =~ ^[0-9]+$ ]]; then
3114
- if [ "$model_input" -ge 1 ] && [ "$model_input" -le ${#ollama_model_list[@]} ]; then
3115
- model_input="${ollama_model_list[$((model_input - 1))]}"
3466
+ local picked_from_menu=""
3467
+ if [ ${#model_menu[@]} -gt 0 ] && [[ "$model_input" =~ ^[0-9]+$ ]]; then
3468
+ if [ "$model_input" -ge 1 ] && [ "$model_input" -le ${#model_menu[@]} ]; then
3469
+ model_input="${model_menu[$((model_input - 1))]}"
3470
+ picked_from_menu="true"
3116
3471
  else
3117
3472
  echo -e "${RED}✗ Invalid selection: ${model_input}${ENDCOLOR}" >&2
3473
+ if [ -n "$_chained" ]; then return 1; fi
3118
3474
  exit 1
3119
3475
  fi
3120
3476
  fi
3121
3477
  if ! is_valid_model_id "$model_input"; then
3122
3478
  echo -e "${RED}✗ Invalid model ID format.${ENDCOLOR}" >&2
3123
3479
  echo -e "${YELLOW}Use only letters, numbers, dots, dashes, underscores, colons, and slashes.${ENDCOLOR}" >&2
3480
+ if [ -n "$_chained" ]; then return 1; fi
3124
3481
  exit 1
3125
3482
  fi
3483
+ if [ -z "$picked_from_menu" ] && [ -n "$model_catalog" ]; then
3484
+ if ! printf '%s\n' "$model_catalog" | LC_ALL=C grep -qxF "$model_input"; then
3485
+ echo -e "${YELLOW}⚠ '$model_input' is not in $(get_ai_provider)'s current model list.${ENDCOLOR}"
3486
+ echo -e "Save it anyway (y/N)?"
3487
+ if ! confirm_destructive; then
3488
+ echo -e "${YELLOW}Not saved. Pick a number from the list or re-check the id.${ENDCOLOR}"
3489
+ if [ -n "$_chained" ]; then return 1; fi
3490
+ exit 1
3491
+ fi
3492
+ fi
3493
+ fi
3494
+ local prev_model
3495
+ prev_model=$(git config --local --get "gitbasher.ai-model-$(get_ai_provider)" 2>/dev/null)
3126
3496
  set_ai_model "$model_input"
3127
- echo -e "${GREEN}✓ Set AI model to '$model_input' for '${project_name}'${ENDCOLOR}"
3497
+ echo -e "${GREEN}✓ Set AI model to '$model_input' for provider '$(get_ai_provider)' in '${project_name}'${ENDCOLOR}"
3128
3498
  echo
3129
- [ "$GITBASHER_NO_REPO" = "true" ] && exit
3130
- echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (y/n)?"
3131
- yes_no_choice "\nSet AI model globally" "true"
3132
- set_config_value gitbasher.ai-model "$model_input" "true"
3499
+ case "$(get_ai_provider)" in
3500
+ openrouter|openai)
3501
+ if ! ai_model_smoke_check; then
3502
+ echo -e "Keep '$model_input' anyway (y/N)?"
3503
+ if ! confirm_destructive; then
3504
+ if [ -n "$prev_model" ]; then
3505
+ git config "gitbasher.ai-model-$(get_ai_provider)" "$prev_model"
3506
+ echo -e "${YELLOW}Restored previous model '${prev_model}'.${ENDCOLOR}"
3507
+ else
3508
+ git config --unset "gitbasher.ai-model-$(get_ai_provider)" 2>/dev/null
3509
+ echo -e "${YELLOW}Removed it — provider '$(get_ai_provider)' uses its default ($(get_ai_default_model)).${ENDCOLOR}"
3510
+ fi
3511
+ if [ -n "$_chained" ]; then return 1; fi
3512
+ exit 1
3513
+ fi
3514
+ fi
3515
+ echo
3516
+ ;;
3517
+ esac
3518
+ if [ "$GITBASHER_NO_REPO" = "true" ]; then
3519
+ if [ -n "$_chained" ]; then return 0; fi
3520
+ exit
3521
+ fi
3522
+ echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (Y/n)?"
3523
+ local _model_global=""
3524
+ read_key _model_global || { if [ -n "$_chained" ]; then return 130; fi; _model_global="n"; }
3525
+ if is_yes "$_model_global"; then
3526
+ echo -e "${YELLOW}Set AI model globally${ENDCOLOR}"
3527
+ set_ai_model "$model_input" "true"
3528
+ fi
3529
+ return 0
3133
3530
  }
3134
- function configure_ai_proxy {
3531
+ function configure_ai_wizard {
3532
+ local _rc
3533
+ echo -e "${YELLOW}Step 1/3 — Provider${ENDCOLOR}"
3534
+ echo
3535
+ configure_ai_provider --chained
3536
+ _rc=$?
3537
+ if [ "$_rc" -eq 130 ]; then prompt_aborted; fi
3538
+ if [ "$_rc" -eq 3 ]; then
3539
+ echo
3540
+ echo -e "${YELLOW}AI setup stopped — nothing changed.${ENDCOLOR}"
3541
+ return 0
3542
+ fi
3543
+ echo
3544
+ echo -e "${YELLOW}Step 2/3 — API key${ENDCOLOR}"
3545
+ echo
3546
+ if ai_provider_requires_api_key; then
3547
+ configure_ai_key --chained
3548
+ _rc=$?
3549
+ if [ "$_rc" -eq 130 ]; then prompt_aborted; fi
3550
+ else
3551
+ echo -e "Provider '$(get_ai_provider)' does not require an API key — skipping."
3552
+ fi
3553
+ echo
3554
+ echo -e "${YELLOW}Step 3/3 — Model${ENDCOLOR}"
3555
+ echo
3556
+ configure_ai_model --chained
3557
+ _rc=$?
3558
+ if [ "$_rc" -eq 130 ]; then prompt_aborted; fi
3559
+ echo
3560
+ echo -e "${GREEN}✓ AI setup complete${ENDCOLOR}"
3561
+ echo
3562
+ print_ai_configuration
3563
+ }
3564
+ function configure_ai_proxy {
3135
3565
  echo -e "${YELLOW}Configure AI HTTP/SOCKS Proxy${ENDCOLOR}"
3136
3566
  echo
3137
3567
  ai_proxy=$(get_ai_proxy)
@@ -3143,9 +3573,9 @@ function configure_ai_proxy {
3143
3573
  echo -e "Enter proxy URL to route AI requests through (useful for bypassing geo-restrictions)"
3144
3574
  echo -e ""
3145
3575
  echo -e "${BLUE}HTTP proxy formats:${ENDCOLOR}"
3146
- echo -e " • ${BLUE}http://proxy.example.com:8080${ENDCOLOR}"
3147
- echo -e " • ${BLUE}http://username:password@proxy.example.com:8080${ENDCOLOR}"
3148
- echo -e " • ${BLUE}http://[2001:db8::1]:8080${ENDCOLOR} (IPv6)"
3576
+ echo -e " • ${BOLD}http://proxy.example.com:8080${ENDCOLOR}"
3577
+ echo -e " • ${BOLD}http://username:password@proxy.example.com:8080${ENDCOLOR}"
3578
+ echo -e " • ${BOLD}http://[2001:db8::1]:8080${ENDCOLOR} (IPv6)"
3149
3579
  echo -e ""
3150
3580
  echo -e "Press Enter to exit without changes, or 0 to remove the existing proxy"
3151
3581
  read_editable_input ai_proxy_input "Proxy URL: "
@@ -3175,7 +3605,7 @@ function configure_ai_proxy {
3175
3605
  echo -e " ${BLUE}gitb commit aif${ENDCOLOR} - Fast AI commit through proxy"
3176
3606
  echo
3177
3607
  [ "$GITBASHER_NO_REPO" = "true" ] && exit
3178
- echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (y/n)?"
3608
+ echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (Y/n)?"
3179
3609
  yes_no_choice "\nSet AI proxy globally" "true"
3180
3610
  set_ai_proxy "$ai_proxy_input"
3181
3611
  git config --global gitbasher.ai-proxy "$ai_proxy_input"
@@ -3209,7 +3639,7 @@ function configure_ai_history {
3209
3639
  if [ "$limit_input" -gt 20 ]; then
3210
3640
  echo -e "${YELLOW}High values may exceed token limits and slow down AI responses.${ENDCOLOR}"
3211
3641
  fi
3212
- read -n 1 -p "Continue anyway? (y/n) " -s choice || choice="n"
3642
+ read -n 1 -p "Continue anyway? (Y/n) " -s choice || choice="n"
3213
3643
  echo
3214
3644
  if ! is_yes "$choice"; then
3215
3645
  exit
@@ -3219,7 +3649,7 @@ function configure_ai_history {
3219
3649
  echo -e "${GREEN}✓ Set AI commit history limit to ${limit_input} for '${project_name}'${ENDCOLOR}"
3220
3650
  echo
3221
3651
  [ "$GITBASHER_NO_REPO" = "true" ] && exit
3222
- echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (y/n)?"
3652
+ echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (Y/n)?"
3223
3653
  yes_no_choice "\nSet AI commit history limit globally" "true"
3224
3654
  git config --global gitbasher.ai-commit-history-limit "$limit_input"
3225
3655
  }
@@ -3257,7 +3687,7 @@ function configure_push_warn_size {
3257
3687
  fi
3258
3688
  echo
3259
3689
  [ "$GITBASHER_NO_REPO" = "true" ] && exit
3260
- echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (y/n)?"
3690
+ echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (Y/n)?"
3261
3691
  yes_no_choice "\nSet push size warning globally" "true"
3262
3692
  git config --global gitbasher.push-warn-size "$limit_input"
3263
3693
  }
@@ -3286,7 +3716,7 @@ function configure_ai_diff {
3286
3716
  lines_input="$validated_number"
3287
3717
  if [ "$lines_input" -lt 50 ] || [ "$lines_input" -gt 2000 ]; then
3288
3718
  echo -e "${YELLOW}⚠ Value is outside the recommended range (50-2000).${ENDCOLOR}"
3289
- read -n 1 -p "Continue anyway? (y/n) " -s choice || choice="n"
3719
+ read -n 1 -p "Continue anyway? (Y/n) " -s choice || choice="n"
3290
3720
  echo
3291
3721
  if ! is_yes "$choice"; then
3292
3722
  exit
@@ -3304,7 +3734,7 @@ function configure_ai_diff {
3304
3734
  chars_input="$validated_number"
3305
3735
  if [ "$chars_input" -lt 4000 ] || [ "$chars_input" -gt 100000 ]; then
3306
3736
  echo -e "${YELLOW}⚠ Value is outside the recommended range (4000-100000).${ENDCOLOR}"
3307
- read -n 1 -p "Continue anyway? (y/n) " -s choice || choice="n"
3737
+ read -n 1 -p "Continue anyway? (Y/n) " -s choice || choice="n"
3308
3738
  echo
3309
3739
  if ! is_yes "$choice"; then
3310
3740
  exit
@@ -3319,7 +3749,7 @@ function configure_ai_diff {
3319
3749
  fi
3320
3750
  echo
3321
3751
  [ "$GITBASHER_NO_REPO" = "true" ] && exit
3322
- echo -e "Do you want to apply ${YELLOW}globally${ENDCOLOR} for all projects (y/n)?"
3752
+ echo -e "Do you want to apply ${YELLOW}globally${ENDCOLOR} for all projects (Y/n)?"
3323
3753
  yes_no_choice "\nApply AI diff settings globally" "true"
3324
3754
  if [ -n "$lines_input" ]; then
3325
3755
  git config --global gitbasher.ai-diff-limit "$lines_input"
@@ -3367,7 +3797,7 @@ function set_scopes {
3367
3797
  echo -e "${GREEN}✓ Set '${scopes}' as the scopes list in '${project_name}'${ENDCOLOR}"
3368
3798
  echo
3369
3799
  [ "$GITBASHER_NO_REPO" = "true" ] && exit
3370
- echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (y/n)?"
3800
+ echo -e "Do you want to set it ${YELLOW}globally${ENDCOLOR} for all projects (Y/n)?"
3371
3801
  yes_no_choice "\nSet '${scopes}' globally" "true"
3372
3802
  git config --global --replace-all gitbasher.scopes "$scopes_raw"
3373
3803
  }
@@ -3400,11 +3830,11 @@ function delete_global {
3400
3830
  _delete_global_add "gitbasher.ai-ollama-host" "Ollama host" "$global_ollama_host" "$GREEN"
3401
3831
  local global_base_url=$(git config --global --get gitbasher.ai-base-url)
3402
3832
  _delete_global_add "gitbasher.ai-base-url" "AI base URL" "$global_base_url" "$GREEN"
3403
- local _task_models
3404
- _task_models=$(git config --global --get-regexp '^gitbasher\.ai-model-' 2>/dev/null | awk '{print $1}' | tr '\n' '+' | sed 's/+$//')
3405
- if [ -n "$_task_models" ]; then
3406
- labels+=("AI per-task models: ${GREEN}configured${ENDCOLOR}")
3407
- actions+=("${_task_models}|AI per-task models")
3833
+ local _provider_models
3834
+ _provider_models=$(git config --global --get-regexp '^gitbasher\.ai-model-' 2>/dev/null | awk '{print $1}' | tr '\n' '+' | sed 's/+$//')
3835
+ if [ -n "$_provider_models" ]; then
3836
+ labels+=("AI models (per provider): ${GREEN}configured${ENDCOLOR}")
3837
+ actions+=("${_provider_models}|AI models (per provider)")
3408
3838
  fi
3409
3839
  while IFS=$'\t' read -r key_name; do
3410
3840
  [ -z "$key_name" ] && continue
@@ -3511,7 +3941,8 @@ function config_script {
3511
3941
  editor|ed|e) set_editor_cfg="true";;
3512
3942
  ticket|jira|ti|t) set_ticket_cfg="true";;
3513
3943
  scopes|scope|sc|s) set_scopes_cfg="true";;
3514
- ai|llm|key) set_ai_cfg="true";;
3944
+ ai|llm) set_ai_cfg="true";;
3945
+ key) set_ai_key_cfg="true";;
3515
3946
  provider|prov) set_ai_provider_cfg="true";;
3516
3947
  model|m) set_ai_model_cfg="true";;
3517
3948
  proxy|prx|p) set_proxy_cfg="true";;
@@ -3536,6 +3967,8 @@ function config_script {
3536
3967
  elif [ -n "${set_scopes_cfg}" ]; then
3537
3968
  header="$header SCOPES LIST"
3538
3969
  elif [ -n "${set_ai_cfg}" ]; then
3970
+ header="$header AI SETUP"
3971
+ elif [ -n "${set_ai_key_cfg}" ]; then
3539
3972
  header="$header AI API KEY"
3540
3973
  elif [ -n "${set_ai_provider_cfg}" ]; then
3541
3974
  header="$header AI PROVIDER"
@@ -3591,6 +4024,10 @@ function config_script {
3591
4024
  exit
3592
4025
  fi
3593
4026
  if [ "$set_ai_cfg" == "true" ]; then
4027
+ configure_ai_wizard
4028
+ exit
4029
+ fi
4030
+ if [ "$set_ai_key_cfg" == "true" ]; then
3594
4031
  configure_ai_key
3595
4032
  exit
3596
4033
  fi
@@ -3638,8 +4075,9 @@ function config_script {
3638
4075
  print_help_row $PAD "editor" "ed, e" "Set the editor for commit messages"
3639
4076
  print_help_row $PAD "ticket" "ti, t, jira" "Set the ticket prefix used in commits and branches"
3640
4077
  print_help_row $PAD "scopes" "sc" "Set the list of suggested commit scopes"
3641
- print_help_row $PAD "ai" "llm, key" "Set the AI API key"
3642
- print_help_row $PAD "provider" "prov" "Choose the AI provider (openrouter, openai, ollama)"
4078
+ print_help_row $PAD "ai" "llm" "Set up AI: provider, API key, and model (wizard)"
4079
+ print_help_row $PAD "key" "" "Set the AI API key only"
4080
+ print_help_row $PAD "provider" "prov" "Choose the AI provider (openrouter, openai, ollama, claude)"
3643
4081
  print_help_row $PAD "model" "m" "Set the AI model override"
3644
4082
  print_help_row $PAD "proxy" "prx, p" "Set an HTTP/SOCKS proxy for AI requests"
3645
4083
  print_help_row $PAD "history" "hist" "Set how many recent commits to include in AI prompts"
@@ -3653,7 +4091,7 @@ function config_script {
3653
4091
  echo -e " ${GREEN}gitb cfg${ENDCOLOR} Show current configuration"
3654
4092
  echo -e " ${GREEN}gitb cfg user${ENDCOLOR} Set user.name and user.email"
3655
4093
  echo -e " ${GREEN}gitb cfg default${ENDCOLOR} Choose the default gitbasher branch"
3656
- echo -e " ${GREEN}gitb cfg ai${ENDCOLOR} Set the AI API key (per-repo or globally)"
4094
+ echo -e " ${GREEN}gitb cfg ai${ENDCOLOR} Interactive AI setup (provider, key, model)"
3657
4095
  exit
3658
4096
  fi
3659
4097
  print_configuration
@@ -3718,7 +4156,7 @@ function merge_script {
3718
4156
  elif [ "$current_local_commit" != "$current_remote_commit" ]; then
3719
4157
  echo -e "${YELLOW}⚠ Remote changes detected on ${current_branch}.${ENDCOLOR}"
3720
4158
  echo
3721
- echo -e "Do you want to pull ${YELLOW}${origin_name}/${current_branch}${ENDCOLOR} first (y/n)?"
4159
+ echo -e "Do you want to pull ${YELLOW}${origin_name}/${current_branch}${ENDCOLOR} first (Y/n)?"
3722
4160
  read -n 1 -s choice || prompt_aborted
3723
4161
  if is_yes "$choice"; then
3724
4162
  echo
@@ -3772,7 +4210,7 @@ function merge_script {
3772
4210
  elif [ "$local_commit" != "$remote_commit" ]; then
3773
4211
  echo -e "${YELLOW}⚠ Remote changes detected.${ENDCOLOR}"
3774
4212
  echo
3775
- echo -e "Do you want to fetch ${YELLOW}${origin_name}/${merge_branch}${ENDCOLOR} before merge (y/n)?"
4213
+ echo -e "Do you want to fetch ${YELLOW}${origin_name}/${merge_branch}${ENDCOLOR} before merge (Y/n)?"
3776
4214
  read -n 1 -s choice || prompt_aborted
3777
4215
  if is_yes "$choice"; then
3778
4216
  echo
@@ -3901,7 +4339,7 @@ function resolve_conflicts {
3901
4339
  if [ "$choice" == "4" ]; then
3902
4340
  echo
3903
4341
  echo -e "${RED}⚠ This will discard your current changes for these files.${ENDCOLOR}"
3904
- echo -e "Are you sure you want to ${GREEN}accept all incoming changes${ENDCOLOR} (y/n)?"
4342
+ echo -e "Are you sure you want to ${GREEN}accept all incoming changes${ENDCOLOR} (y/N)?"
3905
4343
  if confirm_destructive; then
3906
4344
  echo -e "${YELLOW}Accepting all incoming changes...${ENDCOLOR}"
3907
4345
  deleted_files=$(git --no-pager diff --name-only --diff-filter=D --relative 2>/dev/null)
@@ -3909,7 +4347,7 @@ function resolve_conflicts {
3909
4347
  echo -e "${YELLOW}⚠ Some files were deleted in the incoming branch:${ENDCOLOR}"
3910
4348
  echo "$deleted_files" | sed 's/^/\t/'
3911
4349
  echo
3912
- echo -e "Do you want to continue and accept the deletions (y/n)?"
4350
+ echo -e "Do you want to continue and accept the deletions (y/N)?"
3913
4351
  if ! confirm_destructive; then
3914
4352
  echo -e "${YELLOW}Cancelled. Continuing...${ENDCOLOR}"
3915
4353
  continue
@@ -3967,7 +4405,7 @@ function resolve_conflicts {
3967
4405
  if [ "$choice" == "5" ]; then
3968
4406
  echo
3969
4407
  echo -e "${RED}⚠ This will discard the incoming changes for these files.${ENDCOLOR}"
3970
- echo -e "Are you sure you want to ${GREEN}accept all current changes${ENDCOLOR} (y/n)?"
4408
+ echo -e "Are you sure you want to ${GREEN}accept all current changes${ENDCOLOR} (y/N)?"
3971
4409
  if confirm_destructive; then
3972
4410
  echo -e "${YELLOW}Accepting all current changes...${ENDCOLOR}"
3973
4411
  deleted_files=$(git --no-pager diff --name-only --diff-filter=D --relative 2>/dev/null)
@@ -3975,7 +4413,7 @@ function resolve_conflicts {
3975
4413
  echo -e "${YELLOW}⚠ Some files were deleted in the current branch:${ENDCOLOR}"
3976
4414
  echo "$deleted_files" | sed 's/^/\t/'
3977
4415
  echo
3978
- echo -e "Do you want to continue and accept the deletions (y/n)?"
4416
+ echo -e "Do you want to continue and accept the deletions (y/N)?"
3979
4417
  if ! confirm_destructive; then
3980
4418
  echo -e "${YELLOW}Cancelled. Continuing...${ENDCOLOR}"
3981
4419
  continue
@@ -4094,7 +4532,7 @@ ${staged_with_tab}
4094
4532
  echo
4095
4533
  echo -e "${YELLOW}⚠ Merge commit message cannot be empty.${ENDCOLOR}"
4096
4534
  echo
4097
- read -n 1 -p "Do you want to try for one more time? (y/n) " -s -e choice || choice="n"
4535
+ read -n 1 -p "Do you want to try for one more time? (Y/n) " -s -e choice || choice="n"
4098
4536
  if ! is_yes "$choice"; then
4099
4537
  while IFS= read -r _cf; do
4100
4538
  [ -n "$_cf" ] && git restore --staged -- ":(literal)$_cf" 2>/dev/null
@@ -4181,7 +4619,7 @@ function rebase_script {
4181
4619
  elif [ "$current_local_commit" != "$current_remote_commit" ]; then
4182
4620
  echo -e "${YELLOW}⚠ Remote changes detected on ${current_branch}.${ENDCOLOR}"
4183
4621
  echo
4184
- echo -e "Do you want to pull ${YELLOW}${origin_name}/${current_branch}${ENDCOLOR} first (y/n)?"
4622
+ echo -e "Do you want to pull ${YELLOW}${origin_name}/${current_branch}${ENDCOLOR} first (Y/n)?"
4185
4623
  read -n 1 -s choice || prompt_aborted
4186
4624
  if is_yes "$choice"; then
4187
4625
  echo
@@ -4340,7 +4778,7 @@ function rebase_branch {
4340
4778
  fixup_commits_with_dates=$(git log --format="${YELLOW}%h${ENDCOLOR} (${BLUE}%cr${ENDCOLOR}) ${BOLD}%s${ENDCOLOR}" --grep="^fixup!" $ref..HEAD 2>/dev/null || echo "")
4341
4779
  echo -e "$fixup_commits_with_dates" | sed 's/^/\t/'
4342
4780
  echo
4343
- echo -e "Proceed with fast autosquash rebase (y/n)?"
4781
+ echo -e "Proceed with fast autosquash rebase (y/N)?"
4344
4782
  if ! confirm_destructive; then
4345
4783
  echo -e "${YELLOW}Rebase cancelled${ENDCOLOR}"
4346
4784
  exit
@@ -4526,7 +4964,7 @@ function rebase_conflicts {
4526
4964
  if [ "$force_skip" != "true" ]; then
4527
4965
  echo
4528
4966
  echo -e "${RED}⚠ Dropping this commit will permanently remove it from history.${ENDCOLOR}"
4529
- echo -e "Are you sure you want to ${RED}skip and discard${ENDCOLOR} this commit (y/n)?"
4967
+ echo -e "Are you sure you want to ${RED}skip and discard${ENDCOLOR} this commit (y/N)?"
4530
4968
  if ! confirm_destructive; then
4531
4969
  echo -e "${YELLOW}Continuing...${ENDCOLOR}"
4532
4970
  continue
@@ -4545,7 +4983,7 @@ function rebase_conflicts {
4545
4983
  fi
4546
4984
  if [ "$choice" == "4" ]; then
4547
4985
  echo
4548
- echo -e "Are you sure you want to ${YELLOW}abort rebase${ENDCOLOR} (y/n)?"
4986
+ echo -e "Are you sure you want to ${YELLOW}abort rebase${ENDCOLOR} (y/N)?"
4549
4987
  if confirm_destructive; then
4550
4988
  echo -e "${YELLOW}Aborting rebase...${ENDCOLOR}"
4551
4989
  git rebase --abort
@@ -4558,7 +4996,7 @@ function rebase_conflicts {
4558
4996
  if [ "$choice" == "5" ]; then
4559
4997
  echo
4560
4998
  echo -e "${RED}⚠ This will discard your current changes for these files.${ENDCOLOR}"
4561
- echo -e "Are you sure you want to ${GREEN}accept all incoming changes${ENDCOLOR} (y/n)?"
4999
+ echo -e "Are you sure you want to ${GREEN}accept all incoming changes${ENDCOLOR} (y/N)?"
4562
5000
  if confirm_destructive; then
4563
5001
  echo -e "${YELLOW}Accepting all incoming changes...${ENDCOLOR}"
4564
5002
  deleted_files=$(git --no-pager diff --name-only --diff-filter=D --relative 2>/dev/null)
@@ -4566,7 +5004,7 @@ function rebase_conflicts {
4566
5004
  echo -e "${YELLOW}⚠ Some files were deleted in the incoming branch:${ENDCOLOR}"
4567
5005
  echo "$deleted_files" | sed 's/^/\t/'
4568
5006
  echo
4569
- echo -e "Do you want to continue and accept the deletions (y/n)?"
5007
+ echo -e "Do you want to continue and accept the deletions (y/N)?"
4570
5008
  if ! confirm_destructive; then
4571
5009
  echo -e "${YELLOW}Cancelled. Continuing...${ENDCOLOR}"
4572
5010
  continue
@@ -4626,7 +5064,7 @@ function rebase_conflicts {
4626
5064
  if [ "$choice" == "6" ]; then
4627
5065
  echo
4628
5066
  echo -e "${RED}⚠ This will discard the incoming changes for these files.${ENDCOLOR}"
4629
- echo -e "Are you sure you want to ${GREEN}accept all current changes${ENDCOLOR} (y/n)?"
5067
+ echo -e "Are you sure you want to ${GREEN}accept all current changes${ENDCOLOR} (y/N)?"
4630
5068
  if confirm_destructive; then
4631
5069
  echo -e "${YELLOW}Accepting all current changes...${ENDCOLOR}"
4632
5070
  deleted_files=$(git --no-pager diff --name-only --diff-filter=D --relative 2>/dev/null)
@@ -4634,7 +5072,7 @@ function rebase_conflicts {
4634
5072
  echo -e "${YELLOW}⚠ Some files were deleted in the current branch:${ENDCOLOR}"
4635
5073
  echo "$deleted_files" | sed 's/^/\t/'
4636
5074
  echo
4637
- echo -e "Do you want to continue and accept the deletions (y/n)?"
5075
+ echo -e "Do you want to continue and accept the deletions (y/N)?"
4638
5076
  if ! confirm_destructive; then
4639
5077
  echo -e "${YELLOW}Cancelled. Continuing...${ENDCOLOR}"
4640
5078
  continue
@@ -4928,7 +5366,7 @@ function handle_cherry_pick_conflicts {
4928
5366
  ;;
4929
5367
  2)
4930
5368
  echo
4931
- echo -e "Are you sure you want to ${RED}skip${ENDCOLOR} this commit (y/n)?"
5369
+ echo -e "Are you sure you want to ${RED}skip${ENDCOLOR} this commit (y/N)?"
4932
5370
  if confirm_destructive; then
4933
5371
  echo -e "${YELLOW}Skipping commit ${commit_hash::7}${ENDCOLOR}"
4934
5372
  git cherry-pick --skip 2>/dev/null
@@ -4940,7 +5378,7 @@ function handle_cherry_pick_conflicts {
4940
5378
  ;;
4941
5379
  3)
4942
5380
  echo
4943
- echo -e "Are you sure you want to ${GREEN}accept all incoming changes${ENDCOLOR} (y/n)?"
5381
+ echo -e "Are you sure you want to ${GREEN}accept all incoming changes${ENDCOLOR} (y/N)?"
4944
5382
  if confirm_destructive; then
4945
5383
  echo -e "${YELLOW}Accepting all incoming changes...${ENDCOLOR}"
4946
5384
  git checkout --theirs . 2>/dev/null
@@ -4961,7 +5399,7 @@ function handle_cherry_pick_conflicts {
4961
5399
  ;;
4962
5400
  4)
4963
5401
  echo
4964
- echo -e "Are you sure you want to ${GREEN}accept all current changes${ENDCOLOR} (y/n)?"
5402
+ echo -e "Are you sure you want to ${GREEN}accept all current changes${ENDCOLOR} (y/N)?"
4965
5403
  if confirm_destructive; then
4966
5404
  echo -e "${YELLOW}Accepting all current changes...${ENDCOLOR}"
4967
5405
  git checkout --ours . 2>/dev/null
@@ -4982,7 +5420,7 @@ function handle_cherry_pick_conflicts {
4982
5420
  ;;
4983
5421
  5)
4984
5422
  echo
4985
- echo -e "Are you sure you want to ${YELLOW}abort${ENDCOLOR} the pull operation (y/n)?"
5423
+ echo -e "Are you sure you want to ${YELLOW}abort${ENDCOLOR} the pull operation (y/N)?"
4986
5424
  if confirm_destructive; then
4987
5425
  echo -e "${YELLOW}Aborting pull commits operation...${ENDCOLOR}"
4988
5426
  git cherry-pick --abort 2>/dev/null
@@ -5158,7 +5596,7 @@ Re-do the grouping. Every input hash MUST appear in EXACTLY ONE \"commits\" arra
5158
5596
  local max_out=$(( commit_count * 120 ))
5159
5597
  [ "$max_out" -lt 2048 ] && max_out=2048
5160
5598
  [ "$max_out" -gt 16384 ] && max_out=16384
5161
- call_ai_api "$system_prompt" "$user_prompt" "$max_out" "$(get_ai_model_for grouping)" '{"type":"json_object"}'
5599
+ call_ai_api "$system_prompt" "$user_prompt" "$max_out" "" '{"type":"json_object"}' "low"
5162
5600
  }
5163
5601
  function squash_parse_ai_plan {
5164
5602
  local response="$1"
@@ -5570,7 +6008,7 @@ function squash_script {
5570
6008
  size_warning=" ${RED}(large — model accuracy may degrade above ~${SQUASH_MAX_COMMITS} commits)${ENDCOLOR}"
5571
6009
  fi
5572
6010
  echo -e "${YELLOW}Will analyze ${BOLD}${commit_count}${NORMAL}${YELLOW} commits with AI.${ENDCOLOR}${size_warning}"
5573
- echo -e "Continue (y/n)?"
6011
+ echo -e "Continue (Y/n)?"
5574
6012
  local pre_choice
5575
6013
  read -n 1 -s pre_choice || prompt_aborted
5576
6014
  if ! is_yes "$pre_choice"; then
@@ -5620,7 +6058,7 @@ function squash_script {
5620
6058
  if [ -z "$mode_yes" ]; then
5621
6059
  echo -e "${RED}⚠ Applying this plan rewrites your commit history.${ENDCOLOR}"
5622
6060
  echo -e "${CYAN}💡 Recover the original branch with ${BOLD}gitb undo rebase${NORMAL}${CYAN} if needed.${ENDCOLOR}"
5623
- echo -e "Are you sure you want to proceed (y/n)?"
6061
+ echo -e "Are you sure you want to proceed (y/N)?"
5624
6062
  if ! confirm_destructive; then
5625
6063
  echo -e "${YELLOW}Cancelled.${ENDCOLOR}"
5626
6064
  exit 0
@@ -6166,7 +6604,7 @@ function push_script {
6166
6604
  echo -e "${BLUE}Branch ${YELLOW}${current_branch}${BLUE} does not exist on ${YELLOW}${origin_name}${BLUE} yet.${ENDCOLOR}"
6167
6605
  echo
6168
6606
  if [ -z "${fast}" ]; then
6169
- echo -e "Do you want to push the new branch to ${YELLOW}${origin_name}/${current_branch}${ENDCOLOR} (y/n)?"
6607
+ echo -e "Do you want to push the new branch to ${YELLOW}${origin_name}/${current_branch}${ENDCOLOR} (Y/n)?"
6170
6608
  yes_no_choice "Pushing..."
6171
6609
  else
6172
6610
  echo -e "${YELLOW}Pushing...${ENDCOLOR}"
@@ -6229,7 +6667,7 @@ function push_script {
6229
6667
  if [ "${current_branch}" == "${main_branch}" ]; then
6230
6668
  echo -e "${RED}⚠ You are about to push directly to the default branch ${YELLOW}${main_branch}${RED}.${ENDCOLOR}"
6231
6669
  fi
6232
- echo -e "Do you want to push${RED}${force_label}${ENDCOLOR} these commits to ${YELLOW}${origin_name}/${current_branch}${ENDCOLOR} (y/n)?"
6670
+ echo -e "Do you want to push${RED}${force_label}${ENDCOLOR} these commits to ${YELLOW}${origin_name}/${current_branch}${ENDCOLOR} (Y/n)?"
6233
6671
  yes_no_choice "Pushing..."
6234
6672
  else
6235
6673
  echo -e "${YELLOW}Pushing...${ENDCOLOR}"
@@ -6243,7 +6681,7 @@ function push_script {
6243
6681
  echo
6244
6682
  pull "$current_branch" "$origin_name" "$editor" "rebase"
6245
6683
  else
6246
- echo -e "Do you want to pull ${YELLOW}${origin_name}/${current_branch}${ENDCOLOR} (y/n)?"
6684
+ echo -e "Do you want to pull ${YELLOW}${origin_name}/${current_branch}${ENDCOLOR} (Y/n)?"
6247
6685
  yes_no_choice "Pulling..."
6248
6686
  pull "$current_branch" "$origin_name" "$editor"
6249
6687
  fi
@@ -6680,7 +7118,7 @@ A feature is defined by WHAT the change accomplishes, read from the actual code
6680
7118
 
6681
7119
  Input arrives in XML tags: a list of staged files, the actual diff, a diff summary, recent commit messages (for scope-naming conventions), and folder-derived candidate scope tokens.
6682
7120
 
6683
- Task: assign EVERY staged file to exactly one feature group. Use 1 group only if all changes truly form a single feature; otherwise use 2 to ${max_groups} groups. Never exceed ${max_groups} groups — prefer a broader feature over several tiny ones. Name each group with a short lowercase conventional-commit scope describing the FEATURE, matching the style of <recent_commits>. <heuristic_candidates> are folder-derived hints — use one only if it happens to match a feature's purpose. Use \"misc\" only for unrelated incidental files with no feature (e.g., README, top-level config).
7121
+ Task: assign EVERY staged file to exactly one feature group. Use 1 group only if all changes truly form a single feature; otherwise use 2 to ${max_groups} groups. Never exceed ${max_groups} groups — prefer a broader feature over several tiny ones. Name each group with a short lowercase conventional-commit scope describing the FEATURE — the bare scope token only (e.g. \"auth\", never a full prefix like \"feat(auth)\" or a type like \"docs:\"), matching the scope-naming style of <recent_commits>. <heuristic_candidates> are folder-derived hints — use one only if it happens to match a feature's purpose. Use \"misc\" only for unrelated incidental files with no feature (e.g., README, top-level config).
6684
7122
 
6685
7123
  Output format: TSV only. One line per staged file in the form <scope><TAB><file_path>. No header, no prose, no markdown fences, no surrounding quotes. Every file from <staged_files> MUST appear exactly once. The file paths must be byte-identical to those in <staged_files>."
6686
7124
 
@@ -6706,7 +7144,7 @@ ${heuristic_hint}
6706
7144
 
6707
7145
  Read <diff> to decide which files implement the same feature. Output TSV (scope<TAB>file) for every staged file. No prose."
6708
7146
  local ai_response
6709
- ai_response=$(call_ai_api "$system_prompt" "$user_prompt" "$AI_MAX_TOKENS_FULL" "$(get_ai_model_for grouping)" 2>/dev/null)
7147
+ ai_response=$(call_ai_api "$system_prompt" "$user_prompt" "$AI_MAX_TOKENS_FULL" "" "" "low" 2>/dev/null)
6710
7148
  if [ $? -ne 0 ] || [ -z "$ai_response" ]; then
6711
7149
  return 1
6712
7150
  fi
@@ -6726,6 +7164,11 @@ Read <diff> to decide which files implement the same feature. Output TSV (scope<
6726
7164
  [ "$tab_count" -ne 1 ] && continue
6727
7165
  scope="${line%% *}"
6728
7166
  file="${line#* }"
7167
+ scope="${scope%:}"
7168
+ local type_scope_re='^[a-zA-Z0-9._/-]+\(([a-zA-Z0-9._/-]+)\)$'
7169
+ if [[ "$scope" =~ $type_scope_re ]]; then
7170
+ scope="${BASH_REMATCH[1]}"
7171
+ fi
6729
7172
  if ! [[ "$scope" =~ ^[a-zA-Z0-9._/-]+$ ]]; then
6730
7173
  continue
6731
7174
  fi
@@ -7225,6 +7668,19 @@ function handle_ai_commit_generation {
7225
7668
  if [ $? -ne 0 ] || [ -z "$ai_commit_message" ]; then
7226
7669
  echo
7227
7670
  echo -e "${RED}✗ Cannot generate AI commit message.${ENDCOLOR}"
7671
+ echo
7672
+ read_key choice "Continue with a manual commit message? (Y/n) " || choice="n"
7673
+ echo
7674
+ if is_yes "$choice"; then
7675
+ if [ "$ai_mode" = "subject" ]; then
7676
+ echo
7677
+ fi
7678
+ echo -e "${YELLOW}Falling back to manual commit message creation...${ENDCOLOR}"
7679
+ if [ "$ai_mode" = "subject" ]; then
7680
+ echo
7681
+ fi
7682
+ return
7683
+ fi
7228
7684
  cleanup_on_exit "$git_add"
7229
7685
  exit 1
7230
7686
  fi
@@ -7289,7 +7745,7 @@ ${ai_commit_message}"
7289
7745
  echo
7290
7746
  echo -e "${YELLOW}⚠ Commit message cannot be empty.${ENDCOLOR}"
7291
7747
  echo
7292
- read -n 1 -p "Try again? (y/n) " -s -e choice || choice="n"
7748
+ read -n 1 -p "Try again? (Y/n) " -s -e choice || choice="n"
7293
7749
  if ! is_yes "$choice"; then
7294
7750
  cleanup_on_exit "$git_add"
7295
7751
  rm -f "$commitmsg_file"
@@ -7696,7 +8152,7 @@ function commit_script {
7696
8152
  if [ -n "$saved_git_add" ]; then
7697
8153
  echo
7698
8154
  echo -e "${YELLOW}Found previous git add arguments:${ENDCOLOR} ${BOLD}$saved_git_add${ENDCOLOR}"
7699
- read_key choice "Use them? (y/n) " || choice="n"
8155
+ read_key choice "Use them? (Y/n) " || choice="n"
7700
8156
  echo
7701
8157
  if is_yes "$choice"; then
7702
8158
  git add $saved_git_add
@@ -8087,7 +8543,7 @@ ${staged_with_tab}
8087
8543
  echo
8088
8544
  echo -e "${YELLOW}⚠ Commit message cannot be empty.${ENDCOLOR}"
8089
8545
  echo
8090
- read -n 1 -p "Try again? (y/n) " -s -e choice || choice="n"
8546
+ read -n 1 -p "Try again? (Y/n) " -s -e choice || choice="n"
8091
8547
  if ! is_yes "$choice"; then
8092
8548
  cleanup_on_exit "$git_add"
8093
8549
  rm -f "$commitmsg_file"
@@ -8293,7 +8749,7 @@ function edit_branch_name {
8293
8749
  if [ "$old_name" = "$main_branch" ]; then
8294
8750
  echo -e "${YELLOW}⚠ ${BOLD}${old_name}${NORMAL} is the configured main branch.${ENDCOLOR}"
8295
8751
  echo -e "Renaming it will leave ${YELLOW}gitbasher.branch${ENDCOLOR} pointing at the old name."
8296
- printf "Rename anyway? (y/n) "
8752
+ printf "Rename anyway? (y/N) "
8297
8753
  yes_no_choice_strict
8298
8754
  echo
8299
8755
  fi
@@ -8316,7 +8772,7 @@ function edit_branch_name {
8316
8772
  echo
8317
8773
  echo -e "${YELLOW}${BOLD}${old_name}${NORMAL}${YELLOW} also exists on ${origin_name}.${ENDCOLOR}"
8318
8774
  echo -e "Push ${BOLD}${new_name}${NORMAL} and delete the old remote branch?"
8319
- printf "(y/n) "
8775
+ printf "(y/N) "
8320
8776
  yes_no_choice_strict
8321
8777
  echo
8322
8778
  local push_out
@@ -8481,7 +8937,7 @@ function branch_script {
8481
8937
  done
8482
8938
  echo
8483
8939
  echo -e "${RED}⚠ These branches will be force-deleted (${BLUE}git branch -D${RED}).${ENDCOLOR}"
8484
- echo -e "Are you sure you want to delete them (y/n)?"
8940
+ echo -e "Are you sure you want to delete them (y/N)?"
8485
8941
  yes_no_choice_strict "Deleting gone branches..."
8486
8942
  for branch in "${gone_branches[@]}"; do
8487
8943
  delete_output=$(git branch -D "$branch" 2>&1)
@@ -8501,7 +8957,7 @@ function branch_script {
8501
8957
  fi
8502
8958
  if [[ -n "${tag}" ]]; then
8503
8959
  echo -e "${YELLOW}Do you want to fetch remote tags first?${ENDCOLOR}"
8504
- read -n 1 -p "Fetch remote? (y/n) " choice || choice="n"
8960
+ read -n 1 -p "Fetch remote? (Y/n) " choice || choice="n"
8505
8961
  echo
8506
8962
  if [ "$choice" = "y" ] || [ "$choice" = "Y" ]; then
8507
8963
  echo
@@ -8584,7 +9040,7 @@ function branch_script {
8584
9040
  elif [[ -z "$new" ]] && [[ -n "$delete" ]] && [[ -z "$tag" ]]; then
8585
9041
  echo -e "${YELLOW}Clean up local branches that no longer exist on the remote?${ENDCOLOR}"
8586
9042
  echo
8587
- printf "Continue (y/n)? "
9043
+ printf "Continue (Y/n)? "
8588
9044
  while [ true ]; do
8589
9045
  read -n 1 -s choice || prompt_aborted
8590
9046
  if is_yes "$choice"; then
@@ -8614,7 +9070,7 @@ function branch_script {
8614
9070
  done
8615
9071
  echo
8616
9072
  echo -e "${RED}⚠ These branches will be force-deleted (${BLUE}git branch -D${RED}).${ENDCOLOR}"
8617
- printf "Are you sure you want to delete them (y/n)? "
9073
+ printf "Are you sure you want to delete them (y/N)? "
8618
9074
  if confirm_destructive; then
8619
9075
  echo
8620
9076
  for branch in "${branches_to_delete[@]}"; do
@@ -8654,7 +9110,7 @@ function branch_script {
8654
9110
  do
8655
9111
  printf "\t${merged_branches_without_main[index]}\n"
8656
9112
  done
8657
- printf "\nDelete these merged branches (y/n)? "
9113
+ printf "\nDelete these merged branches (Y/n)? "
8658
9114
  while [ true ]; do
8659
9115
  read -n 1 -s choice || prompt_aborted
8660
9116
  if is_yes "$choice"; then
@@ -8722,7 +9178,7 @@ function branch_script {
8722
9178
  done
8723
9179
  echo
8724
9180
  echo -e "${RED}⚠ These will be force-deleted (${BLUE}git branch -D${RED}).${ENDCOLOR}"
8725
- printf "Are you sure (y/n)? "
9181
+ printf "Are you sure (y/N)? "
8726
9182
  if confirm_destructive; then
8727
9183
  echo
8728
9184
  for branch in "${squash_merged_branches[@]}"; do
@@ -8750,7 +9206,7 @@ function branch_script {
8750
9206
  elif [[ ${delete_output} == *"is not fully merged"* ]]; then
8751
9207
  echo -e "${YELLOW}⚠ Branch '$branch_name' is not fully merged.${ENDCOLOR}"
8752
9208
  echo -e "${RED}⚠ Force-delete (${BLUE}git branch -D${RED}) will discard unmerged commits.${ENDCOLOR}"
8753
- printf "Force-delete this branch (y/n)? "
9209
+ printf "Force-delete this branch (y/N)? "
8754
9210
  confirm_destructive
8755
9211
  case $? in
8756
9212
  0)
@@ -8774,7 +9230,7 @@ function branch_script {
8774
9230
  fi
8775
9231
  if git rev-parse --verify --quiet "refs/remotes/$origin_name/$branch_name" >/dev/null 2>&1; then
8776
9232
  echo
8777
- printf "Also delete this branch on ${origin_name} (y/n)? "
9233
+ printf "Also delete this branch on ${origin_name} (y/N)? "
8778
9234
  if confirm_destructive; then
8779
9235
  echo
8780
9236
  echo -e "${YELLOW}Deleting from remote...${ENDCOLOR}"
@@ -8945,7 +9401,7 @@ function branch_script {
8945
9401
  fi
8946
9402
  if [ -n "$origin_name" ]; then
8947
9403
  echo
8948
- echo -e "Do you want to push the new branch to ${YELLOW}${origin_name}/${branch_name}${ENDCOLOR} (y/n)?"
9404
+ echo -e "Do you want to push the new branch to ${YELLOW}${origin_name}/${branch_name}${ENDCOLOR} (Y/n)?"
8949
9405
  yes_no_choice "Pushing..."
8950
9406
  current_branch="$branch_name"
8951
9407
  push -u
@@ -9184,7 +9640,7 @@ function tag_script {
9184
9640
  fi
9185
9641
  if [ -n "${delete}" ] && [ -z "$select" ]; then
9186
9642
  echo
9187
- echo -e "${YELLOW}Do you really want to delete all local tags (y/n)?${ENDCOLOR}"
9643
+ echo -e "${YELLOW}Do you really want to delete all local tags (y/N)?${ENDCOLOR}"
9188
9644
  yes_no_choice_strict "\nDeleting all local tags..."
9189
9645
  local _da_failed=0
9190
9646
  while IFS= read -r _da_tag; do
@@ -9225,7 +9681,7 @@ function tag_script {
9225
9681
  check_code $? "$delete_result" "delete tag"
9226
9682
  echo -e "${GREEN}✓ Deleted tag '${tag_name}'${ENDCOLOR}"
9227
9683
  echo
9228
- echo -e "Also delete this tag on ${YELLOW}${origin_name}${ENDCOLOR} (y/n)?"
9684
+ echo -e "Also delete this tag on ${YELLOW}${origin_name}${ENDCOLOR} (y/N)?"
9229
9685
  yes_no_choice_strict "\nDeleting..."
9230
9686
  push_tag $tag_name "true"
9231
9687
  exit
@@ -9286,7 +9742,7 @@ function tag_script {
9286
9742
  echo
9287
9743
  echo -e "${YELLOW}⚠ Tag message cannot be empty.${ENDCOLOR}"
9288
9744
  echo
9289
- read -n 1 -p "Try again? (y/n) " -s -e choice || choice="n"
9745
+ read -n 1 -p "Try again? (Y/n) " -s -e choice || choice="n"
9290
9746
  if ! is_yes "$choice"; then
9291
9747
  rm -f "$tag_file"
9292
9748
  exit
@@ -9324,7 +9780,7 @@ function tag_script {
9324
9780
  echo -e "$tag_message"
9325
9781
  fi
9326
9782
  echo
9327
- echo -e "Do you want to push it to ${YELLOW}${origin_name}${ENDCOLOR} (y/n)?"
9783
+ echo -e "Do you want to push it to ${YELLOW}${origin_name}${ENDCOLOR} (Y/n)?"
9328
9784
  yes_no_choice "\nPushing..."
9329
9785
  push_tag $tag_name
9330
9786
  }
@@ -9418,7 +9874,7 @@ function reset_script {
9418
9874
  else
9419
9875
  echo -e "${YELLOW}⚠ This will move HEAD; reset changes will be left unstaged.${ENDCOLOR}"
9420
9876
  fi
9421
- echo -e "Are you sure you want to continue (y/n)?"
9877
+ echo -e "Are you sure you want to continue (y/N)?"
9422
9878
  yes_no_choice_strict "Resetting..."
9423
9879
  reset_output=$(git reset "$args" "$move_ref" 2>&1)
9424
9880
  check_code $? "$reset_output" "reset"
@@ -9714,7 +10170,7 @@ function stash_script {
9714
10170
  fi
9715
10171
  echo
9716
10172
  echo -e "${RED}⚠ Dropping stash $selected_stash cannot be undone.${ENDCOLOR}"
9717
- echo -e "Are you sure you want to drop it (y/n)?"
10173
+ echo -e "Are you sure you want to drop it (y/N)?"
9718
10174
  yes_no_choice_strict "Dropping stash"
9719
10175
  drop_output=$(git stash drop "$selected_stash" 2>&1)
9720
10176
  drop_code=$?
@@ -9829,7 +10285,7 @@ function cherry_script {
9829
10285
  echo -e "Files with changes:"
9830
10286
  git status --porcelain | sed 's/^/ /'
9831
10287
  echo
9832
- echo -e "Continue with cherry-pick (may cause conflicts) (y/n)?"
10288
+ echo -e "Continue with cherry-pick (may cause conflicts) (Y/n)?"
9833
10289
  yes_no_choice "Proceeding..."
9834
10290
  echo
9835
10291
  fi
@@ -9909,7 +10365,7 @@ function handle_cherry_pick_conflicts {
9909
10365
  fi
9910
10366
  if [[ $output == *"The previous cherry-pick is now empty"* ]]; then
9911
10367
  echo -e "${YELLOW}This commit is empty after cherry-pick (changes already applied).${ENDCOLOR}"
9912
- echo -e "Skip it (y/n)?"
10368
+ echo -e "Skip it (y/N)?"
9913
10369
  yes_no_choice_strict "Skipping empty commit..."
9914
10370
  skip_output=$(git cherry-pick --skip 2>&1)
9915
10371
  if [ $? == 0 ]; then
@@ -10029,7 +10485,7 @@ function choose_commits_interactive {
10029
10485
  echo -e " ${commit::7} - $message"
10030
10486
  done
10031
10487
  echo
10032
- echo -e "Proceed with cherry-pick? (y/n)"
10488
+ echo -e "Proceed with cherry-pick? (Y/n)"
10033
10489
  yes_no_choice "Proceeding with cherry-pick..."
10034
10490
  local success_count=0
10035
10491
  local total_count=${#selected_commits[@]}
@@ -10272,7 +10728,7 @@ function undo_commit {
10272
10728
  echo -e "\t$cancelled_commit"
10273
10729
  echo
10274
10730
  echo -e "This will undo the last commit but ${GREEN}keep all changes staged${ENDCOLOR}"
10275
- echo -e "Do you want to continue (y/n)?"
10731
+ echo -e "Do you want to continue (y/N)?"
10276
10732
  yes_no_choice_strict "Undoing commit..."
10277
10733
  reset_output=$(git reset --soft HEAD~1 2>&1)
10278
10734
  reset_code=$?
@@ -10321,7 +10777,7 @@ function undo_amend {
10321
10777
  echo -e "${YELLOW}Current (amended):${ENDCOLOR}\t$current_commit"
10322
10778
  echo -e "${GREEN}Restore to:${ENDCOLOR}\t\t$pre_amend_commit"
10323
10779
  echo
10324
- echo -e "Do you want to undo the amend (y/n)?"
10780
+ echo -e "Do you want to undo the amend (y/N)?"
10325
10781
  yes_no_choice_strict "Undoing amend..."
10326
10782
  reset_output=$(git reset --soft "$pre_amend_ref" 2>&1)
10327
10783
  reset_code=$?
@@ -10341,7 +10797,7 @@ function undo_amend {
10341
10797
  function undo_merge {
10342
10798
  if [ -f "$(git rev-parse --git-dir)/MERGE_HEAD" ]; then
10343
10799
  echo -e "${YELLOW}⚠ A merge is in progress.${ENDCOLOR}"
10344
- echo -e "Abort the ongoing merge (y/n)?"
10800
+ echo -e "Abort the ongoing merge (y/N)?"
10345
10801
  yes_no_choice_strict "Aborting merge..."
10346
10802
  abort_output=$(git merge --abort 2>&1)
10347
10803
  abort_code=$?
@@ -10370,7 +10826,7 @@ function undo_merge {
10370
10826
  echo -e "${GREEN}Restore to:${ENDCOLOR}\t$orig_commit"
10371
10827
  echo
10372
10828
  echo -e "${RED}⚠ This will discard the merge commit and all merge changes.${ENDCOLOR}"
10373
- echo -e "Are you sure you want to undo the merge (y/n)?"
10829
+ echo -e "Are you sure you want to undo the merge (y/N)?"
10374
10830
  yes_no_choice_strict "Undoing merge..."
10375
10831
  reset_output=$(git reset --merge ORIG_HEAD 2>&1)
10376
10832
  reset_code=$?
@@ -10387,7 +10843,7 @@ function undo_merge {
10387
10843
  function undo_rebase {
10388
10844
  if [ -d "$(git rev-parse --git-dir)/rebase-merge" ] || [ -d "$(git rev-parse --git-dir)/rebase-apply" ]; then
10389
10845
  echo -e "${YELLOW}⚠ A rebase is in progress.${ENDCOLOR}"
10390
- echo -e "Abort the ongoing rebase (y/n)?"
10846
+ echo -e "Abort the ongoing rebase (y/N)?"
10391
10847
  yes_no_choice_strict "Aborting rebase..."
10392
10848
  abort_output=$(git rebase --abort 2>&1)
10393
10849
  abort_code=$?
@@ -10418,7 +10874,7 @@ function undo_rebase {
10418
10874
  exit 1
10419
10875
  fi
10420
10876
  echo -e "${RED}⚠ This will discard all rebase changes and restore the original branch state.${ENDCOLOR}"
10421
- echo -e "Are you sure you want to undo the rebase (y/n)?"
10877
+ echo -e "Are you sure you want to undo the rebase (y/N)?"
10422
10878
  yes_no_choice_strict "Undoing rebase..."
10423
10879
  reset_output=$(git reset --hard ORIG_HEAD 2>&1)
10424
10880
  reset_code=$?
@@ -10442,7 +10898,7 @@ function undo_stash {
10442
10898
  git_status
10443
10899
  echo
10444
10900
  echo -e "This will stash all current changes to undo the last stash pop/apply."
10445
- echo -e "Continue (y/n)?"
10901
+ echo -e "Continue (y/N)?"
10446
10902
  yes_no_choice_strict "Re-stashing changes..."
10447
10903
  stash_output=$(git stash push -m "undo: re-stashed changes" --include-untracked 2>&1)
10448
10904
  stash_code=$?
@@ -11492,7 +11948,7 @@ function log_commit_actions {
11492
11948
  choice=$((10#$choice))
11493
11949
  local file="${files[choice - 1]}"
11494
11950
  echo -e "${RED}This will overwrite '${file}' in your working tree with its state from ${hash}.${ENDCOLOR}"
11495
- printf "Restore it (y/n)? "
11951
+ printf "Restore it (y/N)? "
11496
11952
  if confirm_destructive; then
11497
11953
  result=$(git checkout "$hash" -- "$file" 2>&1)
11498
11954
  check_code $? "$result" "restore"
@@ -12709,7 +13165,7 @@ function _do_worktree_remove {
12709
13165
  echo -e "${RED}${rm_output}${ENDCOLOR}"
12710
13166
  echo
12711
13167
  echo -e "${YELLOW}Force remove this worktree? (uncommitted changes will be lost)${ENDCOLOR}"
12712
- echo -e "Do you want to continue (y/n)?"
13168
+ echo -e "Do you want to continue (y/N)?"
12713
13169
  yes_no_choice_strict "Force removing worktree..."
12714
13170
  rm_output=$(LC_ALL=C git worktree remove --force --force "$selected_worktree_path" 2>&1)
12715
13171
  rm_code=$?
@@ -12742,7 +13198,7 @@ function worktree_prune {
12742
13198
  echo -e "${YELLOW}Records that would be removed:${ENDCOLOR}"
12743
13199
  echo "$dry_run"
12744
13200
  echo
12745
- echo -e "${YELLOW}Continue? (y/n)${ENDCOLOR}"
13201
+ echo -e "${YELLOW}Continue? (Y/n)${ENDCOLOR}"
12746
13202
  yes_no_choice "Pruning..."
12747
13203
  local prune_output
12748
13204
  prune_output=$(git worktree prune --verbose 2>&1)
@@ -13422,7 +13878,7 @@ function create_hook {
13422
13878
  if [ -f "$hook_file" ]; then
13423
13879
  echo -e "${YELLOW}⚠ Hook '$hook_type' already exists.${ENDCOLOR}"
13424
13880
  echo -e "${RED}⚠ Overwriting will replace its contents.${ENDCOLOR}"
13425
- echo -e "Overwrite (y/n)?"
13881
+ echo -e "Overwrite (y/N)?"
13426
13882
  if ! confirm_destructive; then
13427
13883
  echo -e "${YELLOW}Hook creation cancelled.${ENDCOLOR}"
13428
13884
  return 1
@@ -13468,7 +13924,7 @@ fi
13468
13924
  # lines for any binary blob that happens to be staged.
13469
13925
  if git diff --cached --name-only -z | xargs -0 grep -l --binary-files=without-match \"TODO\\|FIXME\" 2>/dev/null; then
13470
13926
  echo \"⚠️ Warning: Found TODO/FIXME comments in staged files\"
13471
- echo \"Continue anyway? (y/n)\"
13927
+ echo \"Continue anyway? (y/N)\"
13472
13928
  read -n 1 answer
13473
13929
  echo
13474
13930
  if [ \"\$answer\" != \"y\" ] && [ \"\$answer\" != \"Y\" ]; then
@@ -13573,7 +14029,7 @@ exit 0"
13573
14029
  echo -e "${GREEN}✓ Created executable hook: $hook_type${ENDCOLOR}"
13574
14030
  echo -e "${YELLOW}Hook file: $hook_file${ENDCOLOR}"
13575
14031
  echo
13576
- echo -e "Do you want to ${BLUE}edit${ENDCOLOR} the hook now? (y/n)"
14032
+ echo -e "Do you want to ${BLUE}edit${ENDCOLOR} the hook now? (Y/n)"
13577
14033
  read -n 1 -s choice || prompt_aborted
13578
14034
  echo
13579
14035
  if is_yes "$choice"; then
@@ -13644,7 +14100,7 @@ function remove_hook {
13644
14100
  fi
13645
14101
  echo -e "${RED}⚠ Deleting hook '$hook_type' cannot be undone.${ENDCOLOR}"
13646
14102
  echo -e "${GRAY}File: $hook_file${ENDCOLOR}"
13647
- echo -e "Are you sure you want to delete it (y/n)?"
14103
+ echo -e "Are you sure you want to delete it (y/N)?"
13648
14104
  if confirm_destructive; then
13649
14105
  rm "$hook_file"
13650
14106
  echo -e "${GREEN}✓ Removed hook: $hook_type${ENDCOLOR}"
@@ -13685,7 +14141,7 @@ function remove_all_hooks {
13685
14141
  fi
13686
14142
  echo
13687
14143
  echo -e "${RED}⚠ This will permanently delete $hook_count hooks.${ENDCOLOR}"
13688
- echo -e "${YELLOW}Are you absolutely sure (y/n)?${ENDCOLOR}"
14144
+ echo -e "${YELLOW}Are you absolutely sure (Y/n)?${ENDCOLOR}"
13689
14145
  read -n 1 -p "Your choice: " choice < /dev/tty || choice="0"
13690
14146
  echo
13691
14147
  echo
@@ -13816,7 +14272,7 @@ function install_samples {
13816
14272
  fi
13817
14273
  echo
13818
14274
  echo -e "${YELLOW}Ready to install $sample_count sample hooks.${ENDCOLOR}"
13819
- echo -e "Continue (y/n)?"
14275
+ echo -e "Continue (Y/n)?"
13820
14276
  read -n 1 -p "Your choice: " choice < /dev/tty || choice="n"
13821
14277
  echo
13822
14278
  echo
@@ -14264,7 +14720,7 @@ function origin_remove {
14264
14720
  echo -e "URL: ${GRAY}$existing${ENDCOLOR}"
14265
14721
  echo
14266
14722
  echo -e "${RED}⚠ This will remove the remote (you can re-add it later).${ENDCOLOR}"
14267
- echo -e "Are you sure (y/n)?"
14723
+ echo -e "Are you sure (y/N)?"
14268
14724
  yes_no_choice_strict "" "true"
14269
14725
  git remote remove "$target"
14270
14726
  echo
@@ -14423,7 +14879,7 @@ function clone_script {
14423
14879
  echo
14424
14880
  if [ -z "$GITBASHER_TEST_MODE" ] && [ -t 0 ]; then
14425
14881
  local clone_cd_choice
14426
- echo -e "${YELLOW}Enter the new repo now (y/n)?${ENDCOLOR}"
14882
+ echo -e "${YELLOW}Enter the new repo now (Y/n)?${ENDCOLOR}"
14427
14883
  read -n 1 -s clone_cd_choice
14428
14884
  echo
14429
14885
  if is_yes "$clone_cd_choice"; then
@@ -14765,9 +15221,9 @@ function update_script {
14765
15221
  fi
14766
15222
  echo -e "Install location: ${BLUE}${install_path}${ENDCOLOR}"
14767
15223
  if [ $cmp -eq 2 ]; then
14768
- echo -e "Update ${YELLOW}${current_version}${ENDCOLOR} → ${GREEN}${latest_version}${ENDCOLOR}? (y/n)"
15224
+ echo -e "Update ${YELLOW}${current_version}${ENDCOLOR} → ${GREEN}${latest_version}${ENDCOLOR}? (Y/n)"
14769
15225
  else
14770
- echo -e "Reinstall ${GREEN}${latest_version}${ENDCOLOR} over the current binary? (y/n)"
15226
+ echo -e "Reinstall ${GREEN}${latest_version}${ENDCOLOR} over the current binary? (Y/n)"
14771
15227
  fi
14772
15228
  yes_no_choice "Updating..."
14773
15229
  local tmp_path
@@ -15321,7 +15777,7 @@ _gitb() {
15321
15777
  _values 'tag mode' 'annotated[create annotated tag]' 'commit[tag a commit]' 'all[tag everything]' 'push[push tag]' 'push-all[push all tags]' 'delete[delete tag]' 'delete-all[delete all]' 'list[list tags]' 'remote[remote tags]' 'help[show help]'
15322
15778
  ;;
15323
15779
  config|cf|cfg|conf)
15324
- _values 'config key' 'default[default branch]' 'separator[separator char]' 'editor[editor]' 'ticket[ticket prefix]' 'scopes[scope list]' 'ai[AI key]' 'provider[AI provider]' 'model[AI model]' 'proxy[AI proxy]' 'history[AI history]' 'diff[AI diff payload]' 'push-size[warn on large pushes]' 'delete[unset a key]' 'user[user name/email]' 'auto[manage tab completion]' 'help[show help]'
15780
+ _values 'config key' 'default[default branch]' 'separator[separator char]' 'editor[editor]' 'ticket[ticket prefix]' 'scopes[scope list]' 'ai[AI setup wizard]' 'key[AI API key]' 'provider[AI provider]' 'model[AI model]' 'proxy[AI proxy]' 'history[AI history]' 'diff[AI diff payload]' 'push-size[warn on large pushes]' 'delete[unset a key]' 'user[user name/email]' 'auto[manage tab completion]' 'help[show help]'
15325
15781
  ;;
15326
15782
  undo|un)
15327
15783
  _values 'undo target' 'commit[undo last commit]' 'amend[undo amend]' 'merge[undo merge]' 'rebase[undo rebase]' 'stash[undo stash]' 'help[show help]'
@@ -15523,7 +15979,7 @@ complete -c gitb -n "$__gitb_branch" -a "list remote main new newd delete prev r
15523
15979
  set -l __gitb_tag "__gitb_using_cmd tag t tg; and __gitb_at_position 2"
15524
15980
  complete -c gitb -n "$__gitb_tag" -a "annotated commit all push push-all delete delete-all list remote help"
15525
15981
  set -l __gitb_config "__gitb_using_cmd config cf cfg conf; and __gitb_at_position 2"
15526
- complete -c gitb -n "$__gitb_config" -a "default separator editor ticket scopes ai provider model proxy history diff push-size delete user auto help"
15982
+ complete -c gitb -n "$__gitb_config" -a "default separator editor ticket scopes ai key provider model proxy history diff push-size delete user auto help"
15527
15983
  set -l __gitb_undo "__gitb_using_cmd undo un; and __gitb_at_position 2"
15528
15984
  complete -c gitb -n "$__gitb_undo" -a "commit amend merge rebase stash help"
15529
15985
  set -l __gitb_reset "__gitb_using_cmd reset res; and __gitb_at_position 2"