@zachjxyz/moxie 0.4.6 → 0.5.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.
package/bin/moxie CHANGED
@@ -17,7 +17,7 @@
17
17
 
18
18
  set -euo pipefail
19
19
 
20
- MOXIE_VERSION="0.4.6"
20
+ MOXIE_VERSION="0.5.0"
21
21
  # Resolve symlinks (npm installs bin as a symlink)
22
22
  _self="$0"
23
23
  while [ -L "$_self" ]; do
@@ -66,7 +66,7 @@ Commands:
66
66
  cost Token usage breakdown by phase and agent
67
67
  logs Tail or view logs for a phase
68
68
  models List available AI Gateway models
69
- agents List configured agents
69
+ agents List configured agents (agents swap to change models)
70
70
  doctor Check agent CLIs, auth, and project health
71
71
  version Print version
72
72
  help Show this help
@@ -85,6 +85,7 @@ Usage:
85
85
  moxie cost Show token usage summary
86
86
  moxie models List all AI Gateway models
87
87
  moxie models -f anthropic Filter models by provider/name
88
+ moxie agents swap Add/remove/swap gateway models
88
89
  moxie doctor Check agents and dependencies
89
90
 
90
91
  Pipeline: rfc → audit → fix → plan → build
package/lib/agents.sh CHANGED
@@ -537,9 +537,121 @@ dispatch_logged() {
537
537
  _record_turn_health "$agent" "$logfile" || return $?
538
538
  }
539
539
 
540
+ # ---- Rewrite agents section of config.toml ----
541
+ # Preserves [spec], [gateway], [settings] etc. Replaces all [agents.*] blocks.
542
+
543
+ _rewrite_config_agents() {
544
+ # Reads from caller's arrays: cli_agents, cli_cmds, new_gw_names, new_gw_models
545
+ # Use python3 to surgically rewrite just the agents section
546
+ python3 -c "
547
+ import sys, re
548
+
549
+ config_path = sys.argv[1]
550
+ cli_names = sys.argv[2].split('|') if sys.argv[2] else []
551
+ cli_cmds = sys.argv[3].split('|') if sys.argv[3] else []
552
+ gw_names = sys.argv[4].split('|') if sys.argv[4] else []
553
+ gw_models = sys.argv[5].split('|') if sys.argv[5] else []
554
+
555
+ with open(config_path, 'r') as f:
556
+ content = f.read()
557
+
558
+ # Remove all [agents.*] blocks (including their content up to the next section)
559
+ cleaned = re.sub(r'\[agents\.[^\]]+\]\n(?:[^\[]*\n)*', '', content)
560
+
561
+ # Remove old agent comment headers (may span 1-3 lines)
562
+ cleaned = re.sub(r'# Agent definitions[^\n]*\n(?:#[^\n]*\n)*', '', cleaned)
563
+
564
+ # Collapse excessive blank lines
565
+ cleaned = re.sub(r'\n{3,}', '\n\n', cleaned)
566
+
567
+ # Ensure gateway section exists if we have gateway models
568
+ if gw_names and '[gateway]' not in cleaned:
569
+ import datetime, os
570
+ run_id = f'run-{datetime.datetime.now().strftime(\"%Y%m%d-%H%M%S\")}-{os.getpid()}'
571
+ gw_section = f'''[gateway]
572
+ endpoint = \"https://ai-gateway.vercel.sh\"
573
+ run_id = \"{run_id}\"
574
+
575
+ '''
576
+ # Insert before [settings] or at end
577
+ if '[settings]' in cleaned:
578
+ cleaned = cleaned.replace('[settings]', gw_section + '[settings]')
579
+ else:
580
+ cleaned = cleaned.rstrip() + '\n\n' + gw_section
581
+
582
+ # Build new agents section
583
+ agents_lines = []
584
+ agents_lines.append('# Agent definitions — order is the default rotation sequence.')
585
+ agents_lines.append('')
586
+
587
+ order = 1
588
+ for name, cmd in zip(cli_names, cli_cmds):
589
+ if not name:
590
+ continue
591
+ agents_lines.append(f'[agents.{name}]')
592
+ agents_lines.append(f\"command = '{cmd}'\")
593
+ agents_lines.append(f'order = {order}')
594
+ agents_lines.append('')
595
+ order += 1
596
+
597
+ for name, model in zip(gw_names, gw_models):
598
+ if not name:
599
+ continue
600
+ agents_lines.append(f'[agents.{name}]')
601
+ agents_lines.append('type = \"gateway\"')
602
+ agents_lines.append(f'model = \"{model}\"')
603
+ agents_lines.append(f'order = {order}')
604
+ agents_lines.append('')
605
+ order += 1
606
+
607
+ agents_block = '\n'.join(agents_lines)
608
+
609
+ # Insert agents block before [settings] or at end
610
+ if '[settings]' in cleaned:
611
+ cleaned = cleaned.replace('[settings]', agents_block + '\n[settings]')
612
+ else:
613
+ cleaned = cleaned.rstrip() + '\n\n' + agents_block
614
+
615
+ with open(config_path, 'w') as f:
616
+ f.write(cleaned)
617
+ " "$MOXIE_CONFIG" \
618
+ "$(IFS='|'; echo "${cli_agents[*]}")" \
619
+ "$(IFS='|'; echo "${cli_cmds[*]}")" \
620
+ "$(IFS='|'; echo "${new_gw_names[*]}")" \
621
+ "$(IFS='|'; echo "${new_gw_models[*]}")"
622
+ }
623
+
540
624
  # ---- List agents ----
541
625
 
542
626
  cmd_agents() {
627
+ local subcmd="${1:-list}"
628
+ shift 2>/dev/null || true
629
+
630
+ case "$subcmd" in
631
+ list|ls) _cmd_agents_list "$@" ;;
632
+ swap) _cmd_agents_swap "$@" ;;
633
+ help|--help|-h)
634
+ cat <<'EOF'
635
+ Usage: moxie agents [subcommand]
636
+
637
+ Subcommands:
638
+ list List configured agents (default)
639
+ swap Add, remove, or replace gateway models
640
+
641
+ Examples:
642
+ moxie agents Show current agent lineup
643
+ moxie agents swap Interactively swap gateway models
644
+ EOF
645
+ ;;
646
+ *)
647
+ echo "Unknown subcommand: $subcmd" >&2
648
+ echo "Run 'moxie agents help' for usage." >&2
649
+ return 1
650
+ ;;
651
+ esac
652
+ }
653
+
654
+ _cmd_agents_list() {
543
655
  require_moxie_project
544
656
  load_agents
545
657
 
@@ -552,6 +664,297 @@ cmd_agents() {
552
664
  echo " $((i + 1)). $name"
553
665
  echo " $cmd"
554
666
  done
667
+ echo ""
668
+ echo "Swap gateway models anytime: moxie agents swap"
669
+ }
670
+
671
+ # ---- Swap gateway models interactively ----
672
+
673
+ _cmd_agents_swap() {
674
+ require_moxie_project
675
+ load_agents
676
+
677
+ if ! command -v node &>/dev/null; then
678
+ echo "ERROR: node not found on PATH. Required for gateway API calls." >&2
679
+ return 1
680
+ fi
681
+
682
+ # Collect current gateway models from config
683
+ local current_gw_names=()
684
+ local current_gw_models=()
685
+ local cli_agents=()
686
+ local cli_cmds=()
687
+ for i in "${!AGENT_NAMES[@]}"; do
688
+ local name="${AGENT_NAMES[$i]}"
689
+ if _is_gateway_agent "$name"; then
690
+ local model
691
+ model=$(_agent_model "$name")
692
+ current_gw_names+=("$name")
693
+ current_gw_models+=("$model")
694
+ else
695
+ local cmd
696
+ cmd=$(_agent_cmd "$name")
697
+ cli_agents+=("$name")
698
+ cli_cmds+=("$cmd")
699
+ fi
700
+ done
701
+
702
+ echo "Current gateway models:"
703
+ if [ ${#current_gw_names[@]} -eq 0 ]; then
704
+ echo " (none)"
705
+ else
706
+ for i in "${!current_gw_names[@]}"; do
707
+ echo " ${current_gw_names[$i]} — ${current_gw_models[$i]}"
708
+ done
709
+ fi
710
+ echo ""
711
+
712
+ # Ensure we have an API key
713
+ local api_key=""
714
+ if gateway_has_key "vercel-ai-gateway"; then
715
+ api_key=$(gateway_get_key "vercel-ai-gateway" 2>/dev/null) || true
716
+ fi
717
+ if [ -z "$api_key" ]; then
718
+ printf "Gateway API key needed to search models.\\n\\n" >&2
719
+ gateway_store_key "vercel-ai-gateway" || {
720
+ echo "ERROR: Failed to store key." >&2
721
+ return 1
722
+ }
723
+ api_key=$(gateway_get_key "vercel-ai-gateway" 2>/dev/null) || {
724
+ echo "ERROR: Failed to retrieve key." >&2
725
+ return 1
726
+ }
727
+ fi
728
+
729
+ local endpoint="https://ai-gateway.vercel.sh"
730
+ if [ -f "$MOXIE_CONFIG" ]; then
731
+ local cfg_ep
732
+ cfg_ep=$(toml_get "$MOXIE_CONFIG" "gateway.endpoint" "") || true
733
+ [ -n "$cfg_ep" ] && endpoint="$cfg_ep"
734
+ fi
735
+
736
+ # Fetch model catalog
737
+ printf "Fetching models from AI Gateway..." >&2
738
+ local models_json=""
739
+ models_json=$(GATEWAY_API_KEY="$api_key" node "$MOXIE_LIB/gateway-models.mjs" "$endpoint" "" 2>/dev/null) || true
740
+ printf "\\r\\033[K" >&2
741
+
742
+ if [ -z "$models_json" ]; then
743
+ echo "ERROR: Failed to fetch models. Check your API key or network." >&2
744
+ return 1
745
+ fi
746
+
747
+ # Parse into arrays
748
+ local all_model_ids=()
749
+ local all_model_labels=()
750
+ eval "$(python3 -c "
751
+ import json, sys, shlex
752
+ data = json.loads(sys.stdin.read())
753
+ models = data.get('models', [])
754
+ ids = [m['id'] for m in models]
755
+ labels = [m.get('name', m['id']) for m in models]
756
+ print('all_model_ids=(' + ' '.join(shlex.quote(x) for x in ids) + ')')
757
+ print('all_model_labels=(' + ' '.join(shlex.quote(x) for x in labels) + ')')
758
+ " <<< "$models_json" 2>/dev/null)"
759
+
760
+ if [ ${#all_model_ids[@]} -eq 0 ]; then
761
+ echo "ERROR: No models returned from gateway." >&2
762
+ return 1
763
+ fi
764
+
765
+ # Pre-select currently configured models
766
+ local sselected=()
767
+ for (( i = 0; i < ${#all_model_ids[@]}; i++ )); do
768
+ local _matched=0
769
+ for cm in "${current_gw_models[@]}"; do
770
+ if [ "${all_model_ids[$i]}" = "$cm" ]; then
771
+ _matched=1
772
+ break
773
+ fi
774
+ done
775
+ sselected+=("$_matched")
776
+ done
777
+
778
+ # Interactive search TUI (same as init picker)
779
+ local search=""
780
+ local scursor=0
781
+ local max_visible=15
782
+
783
+ _swap_render() {
784
+ local filtered=()
785
+ local lc_search
786
+ lc_search=$(echo "$search" | tr '[:upper:]' '[:lower:]')
787
+ for (( i = 0; i < ${#all_model_ids[@]}; i++ )); do
788
+ if [ -z "$search" ]; then
789
+ filtered+=("$i")
790
+ else
791
+ local lc_id
792
+ lc_id=$(echo "${all_model_ids[$i]}" | tr '[:upper:]' '[:lower:]')
793
+ [[ "$lc_id" == *"$lc_search"* ]] && filtered+=("$i")
794
+ fi
795
+ done
796
+
797
+ local fcount=${#filtered[@]}
798
+ [ "$scursor" -ge "$fcount" ] && scursor=$(( fcount > 0 ? fcount - 1 : 0 ))
799
+ [ "$scursor" -lt 0 ] && scursor=0
800
+
801
+ local vstart=0
802
+ if [ "$scursor" -ge "$max_visible" ]; then
803
+ vstart=$(( scursor - max_visible + 1 ))
804
+ fi
805
+ local vend=$(( vstart + max_visible ))
806
+ [ "$vend" -gt "$fcount" ] && vend=$fcount
807
+
808
+ local sel_count=0
809
+ for (( i = 0; i < ${#sselected[@]}; i++ )); do
810
+ [ "${sselected[$i]}" = "1" ] && sel_count=$(( sel_count + 1 ))
811
+ done
812
+
813
+ printf "\\r\\033[K \\033[1mSwap gateway models\\033[0m (%d available, %d selected)\\n" "$fcount" "$sel_count" >&2
814
+ printf "\\r\\033[K Search: \\033[36m%s\\033[0m\\033[2m|\\033[0m\\n\\n" "$search" >&2
815
+
816
+ local rendered=0
817
+ for (( vi = vstart; vi < vend; vi++ )); do
818
+ local ri=${filtered[$vi]}
819
+ local mid="${all_model_ids[$ri]}"
820
+ local marker=" "
821
+ [ "$vi" -eq "$scursor" ] && marker="> "
822
+ local check="[ ]"
823
+ [ "${sselected[$ri]}" = "1" ] && check="[x]"
824
+ printf "\\r\\033[K %s%s %s\\n" "$marker" "$check" "$mid" >&2
825
+ rendered=$(( rendered + 1 ))
826
+ done
827
+
828
+ while [ "$rendered" -lt "$max_visible" ]; do
829
+ printf "\\r\\033[K\\n" >&2
830
+ rendered=$(( rendered + 1 ))
831
+ done
832
+
833
+ if [ "$vstart" -gt 0 ] || [ "$vend" -lt "$fcount" ]; then
834
+ printf "\\r\\033[K \\033[2m(%d-%d of %d · scroll for more)\\033[0m\\n" "$(( vstart + 1 ))" "$vend" "$fcount" >&2
835
+ else
836
+ printf "\\r\\033[K\\n" >&2
837
+ fi
838
+
839
+ printf "\\r\\033[K \\033[2m↑↓ navigate · space select · enter confirm · esc cancel · type to filter\\033[0m" >&2
840
+ }
841
+
842
+ local swap_lines=$(( max_visible + 5 ))
843
+
844
+ printf "\\033[?25l" >&2
845
+ local old_stty
846
+ old_stty=$(stty -g < /dev/tty 2>/dev/null)
847
+ stty -echo -icanon min 1 < /dev/tty 2>/dev/null
848
+
849
+ _swap_cleanup() {
850
+ printf "\\033[?25h" >&2
851
+ stty "$old_stty" < /dev/tty 2>/dev/null
852
+ }
853
+ trap '_swap_cleanup' INT TERM
854
+
855
+ _swap_render
856
+
857
+ local swap_done=0
858
+ local swap_cancelled=0
859
+ while [ "$swap_done" = "0" ]; do
860
+ local skey
861
+ skey=$(dd bs=1 count=1 2>/dev/null < /dev/tty) || break
862
+
863
+ if [ "$skey" = $'\033' ]; then
864
+ local sbracket sdir
865
+ sbracket=$(dd bs=1 count=1 2>/dev/null < /dev/tty) || true
866
+ sdir=$(dd bs=1 count=1 2>/dev/null < /dev/tty) || true
867
+ if [ "$sbracket" = "[" ]; then
868
+ case "$sdir" in
869
+ A) scursor=$(( scursor - 1 )); [ "$scursor" -lt 0 ] && scursor=0 ;;
870
+ B) scursor=$(( scursor + 1 )) ;;
871
+ esac
872
+ else
873
+ swap_cancelled=1
874
+ swap_done=1
875
+ fi
876
+ elif [ "$skey" = " " ]; then
877
+ local ft=()
878
+ local lcs
879
+ lcs=$(echo "$search" | tr '[:upper:]' '[:lower:]')
880
+ for (( i = 0; i < ${#all_model_ids[@]}; i++ )); do
881
+ if [ -z "$search" ]; then
882
+ ft+=("$i")
883
+ else
884
+ local lci
885
+ lci=$(echo "${all_model_ids[$i]}" | tr '[:upper:]' '[:lower:]')
886
+ [[ "$lci" == *"$lcs"* ]] && ft+=("$i")
887
+ fi
888
+ done
889
+ if [ "$scursor" -lt "${#ft[@]}" ]; then
890
+ local tidx=${ft[$scursor]}
891
+ [ "${sselected[$tidx]}" = "0" ] && sselected[$tidx]=1 || sselected[$tidx]=0
892
+ fi
893
+ elif [ "$skey" = "" ]; then
894
+ swap_done=1
895
+ elif [ "$skey" = $'\177' ] || [ "$skey" = $'\010' ]; then
896
+ if [ -n "$search" ]; then
897
+ search="${search%?}"
898
+ scursor=0
899
+ fi
900
+ else
901
+ if [[ "$skey" =~ [[:print:]] ]]; then
902
+ search="${search}${skey}"
903
+ scursor=0
904
+ fi
905
+ fi
906
+
907
+ printf "\\033[%dA" "$swap_lines" >&2
908
+ _swap_render
909
+ done
910
+
911
+ _swap_cleanup
912
+ trap - INT TERM
913
+ printf "\\n\\n" >&2
914
+
915
+ if [ "$swap_cancelled" = "1" ]; then
916
+ echo "Cancelled — no changes made."
917
+ return 0
918
+ fi
919
+
920
+ # Collect selected models
921
+ local new_gw_names=()
922
+ local new_gw_models=()
923
+ for (( i = 0; i < ${#sselected[@]}; i++ )); do
924
+ [ "${sselected[$i]}" != "1" ] && continue
925
+ local mid="${all_model_ids[$i]}"
926
+ local prov="${mid%%/*}"
927
+ local mname="${mid#*/}"
928
+ local slug
929
+ slug=$(echo "${prov}-${mname}" | tr '[:upper:]' '[:lower:]' | tr ' .' '-' | tr -cd 'a-z0-9-')
930
+ slug="${slug}-gw"
931
+ new_gw_names+=("$slug")
932
+ new_gw_models+=("$mid")
933
+ done
934
+
935
+ local total_agents=$(( ${#cli_agents[@]} + ${#new_gw_names[@]} ))
936
+ if [ "$total_agents" -lt 2 ]; then
937
+ echo "ERROR: Need at least 2 agents total (${#cli_agents[@]} CLI + ${#new_gw_names[@]} gateway = $total_agents)." >&2
938
+ echo "Select more gateway models or install additional CLI agents." >&2
939
+ return 1
940
+ fi
941
+
942
+ # Rewrite config.toml — preserve everything before [agents.*], rewrite agents
943
+ _rewrite_config_agents
944
+
945
+ echo "Updated .moxie/config.toml:"
946
+ echo ""
947
+ local order=1
948
+ for i in "${!cli_agents[@]}"; do
949
+ echo " $order. ${cli_agents[$i]} (CLI)"
950
+ order=$((order + 1))
951
+ done
952
+ for i in "${!new_gw_names[@]}"; do
953
+ echo " $order. ${new_gw_names[$i]} — ${new_gw_models[$i]} (gateway)"
954
+ order=$((order + 1))
955
+ done
956
+ echo ""
957
+ echo "Total: $total_agents agents. Run 'moxie run' or 'moxie start' to go."
555
958
  }
556
959
 
557
960
  # ---- List available gateway models ----
@@ -631,9 +1034,6 @@ for m in models:
631
1034
  p = m['provider']
632
1035
  providers.setdefault(p, []).append(m)
633
1036
 
634
- # Hardcoded models for marking
635
- hardcoded = set('''$(printf '%s\n' "${KNOWN_GATEWAY_MODELS[@]}")'''.strip().split('\n'))
636
-
637
1037
  total = len(models)
638
1038
  filter_note = ' matching \"$filter\"' if '$filter' else ''
639
1039
  print(f'Available gateway models ({total}{filter_note}):')
@@ -643,13 +1043,10 @@ for provider in sorted(providers.keys()):
643
1043
  pmodels = providers[provider]
644
1044
  print(f' \033[1m{provider}\033[0m ({len(pmodels)} models)')
645
1045
  for m in pmodels:
646
- marker = ' \033[32m●\033[0m' if m['id'] in hardcoded else ' '
647
- print(f' {marker} {m[\"id\"]}')
1046
+ print(f' {m[\"id\"]}')
648
1047
  print()
649
1048
 
650
- print('\033[32m●\033[0m = currently in moxie defaults')
651
- print()
652
- print('To use any model, add it to .moxie/config.toml:')
1049
+ print('Use any model with moxie init (search picker) or add to .moxie/config.toml:')
653
1050
  print(' [agents.my-model]')
654
1051
  print(' type = \"gateway\"')
655
1052
  print(' model = \"provider/model-name\"')
package/lib/phases.sh CHANGED
@@ -211,137 +211,203 @@ _select_context_docs() {
211
211
 
212
212
  # ---- Agent selection TUI ----
213
213
  # Populates SELECTED_AGENT_INDICES (CLI) and SELECTED_GATEWAY_INDICES (gateway).
214
- # Shows CLI agents found on PATH + gateway models. Requires minimum 2 total.
214
+ # Shows CLI agents found on PATH + inline gateway model search.
215
+ # Typing filters gateway models live (fzf-style). Requires minimum 2 total.
215
216
 
216
217
  _select_agents() {
217
218
  SELECTED_AGENT_INDICES=()
218
219
  SELECTED_GATEWAY_INDICES=()
219
220
  SELECTED_CUSTOM_GATEWAY_INDICES=()
220
221
 
221
- # Build unified item list: CLI agents, separator, gateway models
222
- local item_labels=()
223
- local item_meta=()
224
- local item_types=() # "cli", "separator", "gateway"
225
- local item_sources=() # index into KNOWN_AGENT_* or KNOWN_GATEWAY_*
222
+ # ---- CLI agent items ----
223
+ local cli_labels=()
224
+ local cli_meta=()
225
+ local cli_sources=()
226
+ local cli_selected=()
226
227
  local max_label_len=0
227
228
 
228
- # CLI agents header
229
- item_labels+=("--- Detected CLI agents ---")
230
- item_meta+=("")
231
- item_types+=("separator")
232
- item_sources+=("-1")
233
-
234
- # CLI agents on PATH
235
229
  for idx in "${AVAILABLE_AGENT_INDICES[@]}"; do
236
230
  local label="${KNOWN_AGENT_LABELS[$idx]}"
237
231
  local binary="${KNOWN_AGENT_BINARIES[$idx]}"
238
- item_labels+=("$label")
232
+ cli_labels+=("$label")
239
233
  local ver
240
234
  ver=$("$binary" --version 2>&1 | head -1 | grep -oE '[0-9]+\.[0-9]+[0-9.]*' | head -1) || ver=""
241
235
  [ -z "$ver" ] && ver="installed"
242
- item_meta+=("$ver")
243
- item_types+=("cli")
244
- item_sources+=("$idx")
245
- local len=${#label}
246
- [ "$len" -gt "$max_label_len" ] && max_label_len=$len
247
- done
248
-
249
- # Separator
250
- item_labels+=("--- Vercel AI Gateway ---")
251
- item_meta+=("")
252
- item_types+=("separator")
253
- item_sources+=("-1")
254
-
255
- # Gateway models (always shown)
256
- for idx in "${!KNOWN_GATEWAY_NAMES[@]}"; do
257
- local label="${KNOWN_GATEWAY_LABELS[$idx]}"
258
- item_labels+=("$label")
259
- item_meta+=("${KNOWN_GATEWAY_MODELS[$idx]}")
260
- item_types+=("gateway")
261
- item_sources+=("$idx")
236
+ cli_meta+=("$ver")
237
+ cli_sources+=("$idx")
238
+ cli_selected+=(1)
262
239
  local len=${#label}
263
240
  [ "$len" -gt "$max_label_len" ] && max_label_len=$len
264
241
  done
265
242
 
266
- # Custom model entry point
267
- item_labels+=("+ Add custom model...")
268
- item_meta+=("type provider/model-name")
269
- item_types+=("custom_add")
270
- item_sources+=("-1")
271
- local _custom_label="+ Add custom model..."
272
- local len=${#_custom_label}
273
- [ "$len" -gt "$max_label_len" ] && max_label_len=$len
274
-
275
- # Track custom models added during this session
276
- local custom_gateway_names=()
277
- local custom_gateway_models=()
278
-
279
- local count=${#item_labels[@]}
280
-
281
- # State: pre-select CLI agents, gateway unselected
282
- local cursor=0
283
- local selected=()
284
- for (( i = 0; i < count; i++ )); do
285
- if [ "${item_types[$i]}" = "cli" ]; then
286
- selected+=(1)
287
- else
288
- selected+=(0)
243
+ local cli_count=${#cli_labels[@]}
244
+
245
+ # ---- Gateway model catalog ----
246
+ local gw_ids=()
247
+ local gw_labels=()
248
+ local gw_selected=()
249
+ local gw_loaded=0
250
+ local gw_error=""
251
+
252
+ # Try to fetch gateway models eagerly if key exists
253
+ if command -v node &>/dev/null && gateway_has_key "vercel-ai-gateway"; then
254
+ local _gw_key=""
255
+ _gw_key=$(gateway_get_key "vercel-ai-gateway" 2>/dev/null) || true
256
+ if [ -n "$_gw_key" ]; then
257
+ printf "Fetching gateway models..." >&2
258
+ local _mj=""
259
+ _mj=$(GATEWAY_API_KEY="$_gw_key" node "$MOXIE_LIB/gateway-models.mjs" "https://ai-gateway.vercel.sh" "" 2>/dev/null) || true
260
+ printf "\\r\\033[K" >&2
261
+ if [ -n "$_mj" ]; then
262
+ eval "$(python3 -c "
263
+ import json, sys, shlex
264
+ data = json.loads(sys.stdin.read())
265
+ models = data.get('models', [])
266
+ ids = [m['id'] for m in models]
267
+ labels = [m.get('name', m['id']) for m in models]
268
+ print('gw_ids=(' + ' '.join(shlex.quote(x) for x in ids) + ')')
269
+ print('gw_labels=(' + ' '.join(shlex.quote(x) for x in labels) + ')')
270
+ " <<< "$_mj" 2>/dev/null)" && gw_loaded=1
271
+ else
272
+ gw_error="Failed to fetch models. Check API key or network."
273
+ fi
289
274
  fi
290
- done
275
+ fi
291
276
 
292
- # Skip separators for initial cursor position
293
- while [ "$cursor" -lt "$count" ] && [ "${item_types[$cursor]}" = "separator" ]; do
294
- cursor=$((cursor + 1))
277
+ local gw_count=${#gw_ids[@]}
278
+ for (( i = 0; i < gw_count; i++ )); do
279
+ gw_selected+=(0)
295
280
  done
296
281
 
282
+ # ---- Layout constants ----
283
+ local gw_search=""
284
+ local gw_cursor=0 # cursor within filtered gateway results
285
+ local MAX_GW_VISIBLE=10 # fixed-height gateway results area
286
+
287
+ # Cursor zones: "cli" (index 0..cli_count-1), "gw" (filtered results), "submit"
288
+ local zone="cli"
289
+ local cli_cursor=0
290
+ # Skip to first CLI agent if available
291
+ [ "$cli_count" -eq 0 ] && zone="gw"
292
+
297
293
  local sep=""
298
- for (( i = 0; i < max_label_len + 40; i++ )); do sep="${sep}-"; done
294
+ for (( i = 0; i < max_label_len + 50; i++ )); do sep="${sep}-"; done
295
+
296
+ # Fixed total lines: header(1) + cli(cli_count) + sep(1) + search(1) + gw_area(MAX_GW_VISIBLE) + scroll(1) + sep(1) + submit(1) + blank(1) + hint(1)
297
+ local total_lines=$(( 1 + cli_count + 1 + 1 + MAX_GW_VISIBLE + 1 + 1 + 1 + 1 + 1 ))
298
+
299
+ _build_gw_filtered() {
300
+ # Populates gw_filtered array with indices matching gw_search
301
+ gw_filtered=()
302
+ local lc_search
303
+ lc_search=$(echo "$gw_search" | tr '[:upper:]' '[:lower:]')
304
+ for (( i = 0; i < gw_count; i++ )); do
305
+ if [ -z "$gw_search" ]; then
306
+ gw_filtered+=("$i")
307
+ else
308
+ local lc_id
309
+ lc_id=$(echo "${gw_ids[$i]}" | tr '[:upper:]' '[:lower:]')
310
+ if [[ "$lc_id" == *"$lc_search"* ]]; then
311
+ gw_filtered+=("$i")
312
+ fi
313
+ fi
314
+ done
315
+ }
299
316
 
300
- # submit row is at index $count
301
- local jump_back=$(( count + 3 ))
317
+ local gw_filtered=()
318
+ _build_gw_filtered
302
319
 
303
- _agent_render() {
320
+ _sa_render() {
321
+ # Count total selected
304
322
  local sel_count=0
305
- for (( i = 0; i < count; i++ )); do
306
- [ "${selected[$i]}" = "1" ] && sel_count=$(( sel_count + 1 ))
323
+ for (( i = 0; i < cli_count; i++ )); do
324
+ [ "${cli_selected[$i]}" = "1" ] && sel_count=$(( sel_count + 1 ))
325
+ done
326
+ for (( i = 0; i < gw_count; i++ )); do
327
+ [ "${gw_selected[$i]}" = "1" ] && sel_count=$(( sel_count + 1 ))
307
328
  done
308
329
 
309
- for (( i = 0; i < count; i++ )); do
310
- if [ "${item_types[$i]}" = "separator" ]; then
311
- printf "\\r\\033[K \\033[2m%s\\033[0m\\n" "${item_labels[$i]}" >&2
312
- continue
313
- fi
330
+ # CLI header
331
+ printf "\\r\\033[K \\033[2m--- Detected CLI agents ---\\033[0m\\n" >&2
332
+
333
+ # CLI agents
334
+ for (( i = 0; i < cli_count; i++ )); do
314
335
  local marker=" "
315
- [ "$cursor" -eq "$i" ] && marker="> "
316
- if [ "${item_types[$i]}" = "custom_add" ]; then
317
- printf "\\r\\033[K %s \\033[36m%s\\033[0m \\033[2m(%s)\\033[0m\\n" "$marker" "${item_labels[$i]}" "${item_meta[$i]}" >&2
318
- continue
319
- fi
336
+ [ "$zone" = "cli" ] && [ "$cli_cursor" -eq "$i" ] && marker="> "
320
337
  local check="[ ]"
321
- [ "${selected[$i]}" = "1" ] && check="[x]"
322
- if [ -n "${item_meta[$i]}" ]; then
323
- printf "\\r\\033[K %s%s %-${max_label_len}s (%s)\\n" "$marker" "$check" "${item_labels[$i]}" "${item_meta[$i]}" >&2
338
+ [ "${cli_selected[$i]}" = "1" ] && check="[x]"
339
+ printf "\\r\\033[K %s%s %-${max_label_len}s (%s)\\n" "$marker" "$check" "${cli_labels[$i]}" "${cli_meta[$i]}" >&2
340
+ done
341
+
342
+ # Gateway header + search
343
+ printf "\\r\\033[K \\033[2m--- Vercel AI Gateway ---\\033[0m\\n" >&2
344
+
345
+ if [ "$gw_loaded" = "1" ]; then
346
+ printf "\\r\\033[K Search: \\033[36m%s\\033[0m\\033[2m|\\033[0m \\033[2m(%d of %d)\\033[0m\\n" "$gw_search" "${#gw_filtered[@]}" "$gw_count" >&2
347
+
348
+ # Clamp gw_cursor
349
+ local fcount=${#gw_filtered[@]}
350
+ [ "$gw_cursor" -ge "$fcount" ] && gw_cursor=$(( fcount > 0 ? fcount - 1 : 0 ))
351
+ [ "$gw_cursor" -lt 0 ] && gw_cursor=0
352
+
353
+ # Scroll window
354
+ local vstart=0
355
+ if [ "$gw_cursor" -ge "$MAX_GW_VISIBLE" ]; then
356
+ vstart=$(( gw_cursor - MAX_GW_VISIBLE + 1 ))
357
+ fi
358
+ local vend=$(( vstart + MAX_GW_VISIBLE ))
359
+ [ "$vend" -gt "$fcount" ] && vend=$fcount
360
+
361
+ local rendered=0
362
+ for (( vi = vstart; vi < vend; vi++ )); do
363
+ local ri=${gw_filtered[$vi]}
364
+ local marker=" "
365
+ [ "$zone" = "gw" ] && [ "$vi" -eq "$gw_cursor" ] && marker="> "
366
+ local check="[ ]"
367
+ [ "${gw_selected[$ri]}" = "1" ] && check="[x]"
368
+ printf "\\r\\033[K %s%s %s\\n" "$marker" "$check" "${gw_ids[$ri]}" >&2
369
+ rendered=$(( rendered + 1 ))
370
+ done
371
+
372
+ # Pad to fixed height
373
+ while [ "$rendered" -lt "$MAX_GW_VISIBLE" ]; do
374
+ printf "\\r\\033[K\\n" >&2
375
+ rendered=$(( rendered + 1 ))
376
+ done
377
+
378
+ # Scroll indicator
379
+ if [ "$fcount" -gt "$MAX_GW_VISIBLE" ]; then
380
+ printf "\\r\\033[K \\033[2m(%d-%d of %d · ↑↓ to scroll)\\033[0m\\n" "$(( vstart + 1 ))" "$vend" "$fcount" >&2
324
381
  else
325
- printf "\\r\\033[K %s%s %s\\n" "$marker" "$check" "${item_labels[$i]}" >&2
382
+ printf "\\r\\033[K\\n" >&2
326
383
  fi
327
- done
384
+ elif [ -n "$gw_error" ]; then
385
+ printf "\\r\\033[K \\033[31m%s\\033[0m\\n" "$gw_error" >&2
386
+ for (( i = 0; i < MAX_GW_VISIBLE; i++ )); do printf "\\r\\033[K\\n" >&2; done
387
+ printf "\\r\\033[K\\n" >&2
388
+ else
389
+ printf "\\r\\033[K \\033[2mNo API key configured. Select CLI agents or set up a key with 'moxie init'.\\033[0m\\n" >&2
390
+ for (( i = 0; i < MAX_GW_VISIBLE; i++ )); do printf "\\r\\033[K\\n" >&2; done
391
+ printf "\\r\\033[K\\n" >&2
392
+ fi
328
393
 
394
+ # Separator + submit
329
395
  printf "\\r\\033[K %s\\n" "$sep" >&2
330
396
 
331
397
  local submit_marker=" "
332
- [ "$cursor" -eq "$count" ] && submit_marker="> "
398
+ [ "$zone" = "submit" ] && submit_marker="> "
333
399
  if [ "$sel_count" -ge 2 ]; then
334
400
  printf "\\r\\033[K %s%s\\n" "$submit_marker" "Submit ($sel_count selected)" >&2
335
401
  else
336
402
  printf "\\r\\033[K %s\\033[2m%s\\033[0m\\n" "$submit_marker" "Submit (need at least 2)" >&2
337
403
  fi
338
404
 
339
- printf "\\r\\033[K\\n\\033[K \\033[2m↑↓ navigate · space toggle · enter submit/add · 'moxie models' to browse\\033[0m" >&2
405
+ printf "\\r\\033[K\\n\\033[K \\033[2m↑↓ navigate · space toggle · type to search · enter submit\\033[0m" >&2
340
406
  }
341
407
 
342
- printf "Select agents (at least 2). CLI agents pre-selected, gateway models available below:\\n\\n" >&2
408
+ printf "Select agents (at least 2):\\n\\n" >&2
343
409
  printf "\\033[?25l" >&2
344
- _agent_render
410
+ _sa_render
345
411
 
346
412
  local old_stty
347
413
  old_stty=$(stty -g < /dev/tty 2>/dev/null)
@@ -362,134 +428,130 @@ _select_agents() {
362
428
  dir=$(dd bs=1 count=1 2>/dev/null < /dev/tty) || true
363
429
  if [ "$bracket" = "[" ]; then
364
430
  case "$dir" in
365
- A) # Up: skip separators
366
- local new_cursor=$((cursor - 1))
367
- while [ "$new_cursor" -ge 0 ] && [ "${item_types[$new_cursor]}" = "separator" ]; do
368
- new_cursor=$((new_cursor - 1))
369
- done
370
- [ "$new_cursor" -ge 0 ] && cursor=$new_cursor
431
+ A) # Up
432
+ if [ "$zone" = "submit" ]; then
433
+ if [ "$gw_loaded" = "1" ] && [ ${#gw_filtered[@]} -gt 0 ]; then
434
+ zone="gw"
435
+ gw_cursor=$(( ${#gw_filtered[@]} - 1 ))
436
+ elif [ "$cli_count" -gt 0 ]; then
437
+ zone="cli"
438
+ cli_cursor=$(( cli_count - 1 ))
439
+ fi
440
+ elif [ "$zone" = "gw" ]; then
441
+ if [ "$gw_cursor" -gt 0 ]; then
442
+ gw_cursor=$(( gw_cursor - 1 ))
443
+ elif [ "$cli_count" -gt 0 ]; then
444
+ zone="cli"
445
+ cli_cursor=$(( cli_count - 1 ))
446
+ fi
447
+ elif [ "$zone" = "cli" ]; then
448
+ [ "$cli_cursor" -gt 0 ] && cli_cursor=$(( cli_cursor - 1 ))
449
+ fi
371
450
  ;;
372
- B) # Down: skip separators
373
- local new_cursor=$((cursor + 1))
374
- while [ "$new_cursor" -lt "$count" ] && [ "${item_types[$new_cursor]}" = "separator" ]; do
375
- new_cursor=$((new_cursor + 1))
376
- done
377
- [ "$new_cursor" -le "$count" ] && cursor=$new_cursor
451
+ B) # Down
452
+ if [ "$zone" = "cli" ]; then
453
+ if [ "$cli_cursor" -lt $(( cli_count - 1 )) ]; then
454
+ cli_cursor=$(( cli_cursor + 1 ))
455
+ elif [ "$gw_loaded" = "1" ] && [ ${#gw_filtered[@]} -gt 0 ]; then
456
+ zone="gw"
457
+ gw_cursor=0
458
+ else
459
+ zone="submit"
460
+ fi
461
+ elif [ "$zone" = "gw" ]; then
462
+ if [ "$gw_cursor" -lt $(( ${#gw_filtered[@]} - 1 )) ]; then
463
+ gw_cursor=$(( gw_cursor + 1 ))
464
+ else
465
+ zone="submit"
466
+ fi
467
+ fi
378
468
  ;;
379
469
  esac
380
470
  fi
381
471
  elif [ "$key" = " " ]; then
382
- if [ "$cursor" -lt "$count" ] && [ "${item_types[$cursor]}" != "separator" ] && [ "${item_types[$cursor]}" != "custom_add" ]; then
383
- [ "${selected[$cursor]}" = "0" ] && selected[$cursor]=1 || selected[$cursor]=0
472
+ # Toggle selection
473
+ if [ "$zone" = "cli" ]; then
474
+ [ "${cli_selected[$cli_cursor]}" = "0" ] && cli_selected[$cli_cursor]=1 || cli_selected[$cli_cursor]=0
475
+ elif [ "$zone" = "gw" ] && [ ${#gw_filtered[@]} -gt 0 ]; then
476
+ local ri=${gw_filtered[$gw_cursor]}
477
+ [ "${gw_selected[$ri]}" = "0" ] && gw_selected[$ri]=1 || gw_selected[$ri]=0
384
478
  fi
385
479
  elif [ "$key" = "" ]; then
386
- if [ "$cursor" -eq "$count" ]; then
387
- # Submit
480
+ # Enter — submit if on submit row, else toggle
481
+ if [ "$zone" = "submit" ]; then
388
482
  local sel_count=0
389
- for (( i = 0; i < count; i++ )); do
390
- [ "${selected[$i]}" = "1" ] && sel_count=$(( sel_count + 1 ))
483
+ for (( i = 0; i < cli_count; i++ )); do
484
+ [ "${cli_selected[$i]}" = "1" ] && sel_count=$(( sel_count + 1 ))
485
+ done
486
+ for (( i = 0; i < gw_count; i++ )); do
487
+ [ "${gw_selected[$i]}" = "1" ] && sel_count=$(( sel_count + 1 ))
391
488
  done
392
489
  [ "$sel_count" -ge 2 ] && break
393
- elif [ "${item_types[$cursor]}" = "custom_add" ]; then
394
- # Prompt for custom model string
395
- printf "\\033[?25h" >&2
396
- stty echo icanon < /dev/tty 2>/dev/null
397
- printf "\\n\\r\\033[K Model ID (provider/model-name): " >&2
398
- local custom_model=""
399
- read -r custom_model < /dev/tty
400
- stty -echo -icanon min 1 < /dev/tty 2>/dev/null
401
- printf "\\033[?25l" >&2
402
-
403
- if [ -n "$custom_model" ] && [[ "$custom_model" == */* ]]; then
404
- # Derive a short name from the model ID: provider-modelbase
405
- local custom_provider="${custom_model%%/*}"
406
- local custom_name_part="${custom_model#*/}"
407
- local custom_slug="${custom_provider}-${custom_name_part}"
408
- # Sanitize slug for TOML key
409
- custom_slug=$(echo "$custom_slug" | tr '[:upper:]' '[:lower:]' | tr ' .' '-' | tr -cd 'a-z0-9-')
410
- custom_slug="${custom_slug}-gw"
411
-
412
- # Insert before the custom_add row (which is at current cursor)
413
- local insert_at=$cursor
414
-
415
- # Shift arrays to insert new item
416
- local new_labels=() new_meta=() new_types=() new_sources=() new_selected=()
417
- for (( i = 0; i < count; i++ )); do
418
- if [ "$i" -eq "$insert_at" ]; then
419
- new_labels+=("$custom_name_part")
420
- new_meta+=("$custom_model")
421
- new_types+=("custom_gateway")
422
- new_sources+=("${#custom_gateway_names[@]}")
423
- new_selected+=(1)
424
- fi
425
- new_labels+=("${item_labels[$i]}")
426
- new_meta+=("${item_meta[$i]}")
427
- new_types+=("${item_types[$i]}")
428
- new_sources+=("${item_sources[$i]}")
429
- new_selected+=("${selected[$i]}")
430
- done
431
-
432
- item_labels=("${new_labels[@]}")
433
- item_meta=("${new_meta[@]}")
434
- item_types=("${new_types[@]}")
435
- item_sources=("${new_sources[@]}")
436
- selected=("${new_selected[@]}")
437
-
438
- custom_gateway_names+=("$custom_slug")
439
- custom_gateway_models+=("$custom_model")
440
-
441
- count=${#item_labels[@]}
442
- jump_back=$(( count + 3 ))
443
-
444
- # Update max_label_len if needed
445
- local len=${#custom_name_part}
446
- [ "$len" -gt "$max_label_len" ] && max_label_len=$len
447
- sep=""
448
- for (( i = 0; i < max_label_len + 40; i++ )); do sep="${sep}-"; done
449
- else
450
- # Invalid — erase the prompt line
451
- if [ -n "$custom_model" ]; then
452
- printf "\\r\\033[K \\033[31mInvalid format. Use provider/model-name (e.g. anthropic/claude-sonnet-4-6)\\033[0m" >&2
453
- sleep 1
454
- fi
490
+ elif [ "$zone" = "cli" ]; then
491
+ [ "${cli_selected[$cli_cursor]}" = "0" ] && cli_selected[$cli_cursor]=1 || cli_selected[$cli_cursor]=0
492
+ elif [ "$zone" = "gw" ] && [ ${#gw_filtered[@]} -gt 0 ]; then
493
+ local ri=${gw_filtered[$gw_cursor]}
494
+ [ "${gw_selected[$ri]}" = "0" ] && gw_selected[$ri]=1 || gw_selected[$ri]=0
495
+ fi
496
+ elif [ "$key" = $'\177' ] || [ "$key" = $'\010' ]; then
497
+ # Backspace remove from search
498
+ if [ -n "$gw_search" ]; then
499
+ gw_search="${gw_search%?}"
500
+ gw_cursor=0
501
+ _build_gw_filtered
502
+ # Jump to gateway zone if we have results
503
+ if [ ${#gw_filtered[@]} -gt 0 ]; then
504
+ zone="gw"
455
505
  fi
456
- # Erase the prompt lines and re-render
457
- printf "\\033[2A" >&2
458
- elif [ "${item_types[$cursor]}" != "separator" ]; then
459
- [ "${selected[$cursor]}" = "0" ] && selected[$cursor]=1 || selected[$cursor]=0
460
506
  fi
461
- elif [ "$key" = "q" ]; then
462
- for (( i = 0; i < count; i++ )); do selected[$i]=0; done
507
+ elif [ "$key" = "q" ] && [ -z "$gw_search" ]; then
508
+ # Quit only when search is empty (otherwise 'q' is a search char)
509
+ for (( i = 0; i < cli_count; i++ )); do cli_selected[$i]=0; done
510
+ for (( i = 0; i < gw_count; i++ )); do gw_selected[$i]=0; done
463
511
  break
512
+ elif [[ "$key" =~ [[:print:]] ]] && [ "$gw_loaded" = "1" ]; then
513
+ # Printable char — append to gateway search
514
+ gw_search="${gw_search}${key}"
515
+ gw_cursor=0
516
+ _build_gw_filtered
517
+ # Auto-switch to gateway zone
518
+ if [ ${#gw_filtered[@]} -gt 0 ]; then
519
+ zone="gw"
520
+ fi
464
521
  fi
465
522
 
466
- printf "\\033[%dA" "$jump_back" >&2
467
- _agent_render
523
+ printf "\\033[%dA" "$total_lines" >&2
524
+ _sa_render
468
525
  done
469
526
 
470
527
  _agent_cleanup
471
528
  trap - INT TERM
472
529
  printf "\\n\\n" >&2
473
530
 
474
- # Export custom models for config writer
475
- CUSTOM_GATEWAY_NAMES=("${custom_gateway_names[@]}")
476
- CUSTOM_GATEWAY_MODELS=("${custom_gateway_models[@]}")
531
+ # ---- Collect selections ----
532
+ for (( i = 0; i < cli_count; i++ )); do
533
+ [ "${cli_selected[$i]}" = "1" ] && SELECTED_AGENT_INDICES+=("${cli_sources[$i]}")
534
+ done
477
535
 
478
- # Collect selections by type
536
+ # Build custom gateway selections
537
+ CUSTOM_GATEWAY_NAMES=()
538
+ CUSTOM_GATEWAY_MODELS=()
539
+ SELECTED_CUSTOM_GATEWAY_INDICES=()
479
540
  local has_gateway=0
480
- for (( i = 0; i < count; i++ )); do
481
- [ "${selected[$i]}" != "1" ] && continue
482
- case "${item_types[$i]}" in
483
- cli) SELECTED_AGENT_INDICES+=("${item_sources[$i]}") ;;
484
- gateway)
485
- SELECTED_GATEWAY_INDICES+=("${item_sources[$i]}")
486
- has_gateway=1
487
- ;;
488
- custom_gateway)
489
- SELECTED_CUSTOM_GATEWAY_INDICES+=("${item_sources[$i]}")
490
- has_gateway=1
491
- ;;
492
- esac
541
+ local gw_order=0
542
+ for (( i = 0; i < gw_count; i++ )); do
543
+ [ "${gw_selected[$i]}" != "1" ] && continue
544
+ has_gateway=1
545
+ local mid="${gw_ids[$i]}"
546
+ local prov="${mid%%/*}"
547
+ local mname="${mid#*/}"
548
+ local slug
549
+ slug=$(echo "${prov}-${mname}" | tr '[:upper:]' '[:lower:]' | tr ' .' '-' | tr -cd 'a-z0-9-')
550
+ slug="${slug}-gw"
551
+ CUSTOM_GATEWAY_NAMES+=("$slug")
552
+ CUSTOM_GATEWAY_MODELS+=("$mid")
553
+ SELECTED_CUSTOM_GATEWAY_INDICES+=("$gw_order")
554
+ gw_order=$(( gw_order + 1 ))
493
555
  done
494
556
 
495
557
  # If gateway models selected, ensure API key is stored
@@ -497,8 +559,9 @@ _select_agents() {
497
559
  printf "Gateway models selected. Setting up API key...\\n\\n" >&2
498
560
  gateway_store_key "vercel-ai-gateway" || {
499
561
  echo "ERROR: Failed to store gateway key. Gateway models will not work." >&2
500
- SELECTED_GATEWAY_INDICES=()
501
562
  SELECTED_CUSTOM_GATEWAY_INDICES=()
563
+ CUSTOM_GATEWAY_NAMES=()
564
+ CUSTOM_GATEWAY_MODELS=()
502
565
  }
503
566
  fi
504
567
  }
@@ -681,11 +744,15 @@ cmd_init() {
681
744
  # Non-interactive: auto-select all available CLI agents
682
745
  SELECTED_AGENT_INDICES=("${AVAILABLE_AGENT_INDICES[@]}")
683
746
  SELECTED_GATEWAY_INDICES=()
747
+ SELECTED_CUSTOM_GATEWAY_INDICES=()
748
+ CUSTOM_GATEWAY_NAMES=()
749
+ CUSTOM_GATEWAY_MODELS=()
684
750
  fi
685
751
 
686
752
  local _cli_count=${#SELECTED_AGENT_INDICES[@]}
687
753
  local _gw_count=${#SELECTED_GATEWAY_INDICES[@]}
688
- local total_selected=$(( _cli_count + _gw_count ))
754
+ local _cgw_count=${#SELECTED_CUSTOM_GATEWAY_INDICES[@]}
755
+ local total_selected=$(( _cli_count + _gw_count + _cgw_count ))
689
756
  if [ "$total_selected" -lt 2 ]; then
690
757
  echo "ERROR: At least 2 agents must be selected." >&2
691
758
  echo "moxie requires at least 2 agents for cross-model verification." >&2
@@ -716,6 +783,9 @@ cmd_init() {
716
783
  for idx in "${SELECTED_GATEWAY_INDICES[@]}"; do
717
784
  echo " - ${KNOWN_GATEWAY_LABELS[$idx]} (${KNOWN_GATEWAY_MODELS[$idx]}, AI Gateway)"
718
785
  done
786
+ for idx in "${SELECTED_CUSTOM_GATEWAY_INDICES[@]}"; do
787
+ echo " - ${CUSTOM_GATEWAY_MODELS[$idx]} (AI Gateway)"
788
+ done
719
789
  echo ""
720
790
 
721
791
  # Create directory structure
@@ -782,13 +852,13 @@ SEED
782
852
 
783
853
  echo ""
784
854
  echo "Next steps:"
785
- echo " 1. Run: moxie doctor (verify dependencies and environment)"
786
- echo " 2. Review .moxie/config.toml (agent commands, timeouts)"
855
+ echo " 1. Run: moxie doctor (verify dependencies and environment)"
856
+ echo " 2. Run: moxie agents swap (add or change gateway models anytime)"
787
857
  if [ ! -d "$MOXIE_DIR/context" ] || [ -z "$(ls -A "$MOXIE_DIR/context" 2>/dev/null)" ]; then
788
858
  echo " Tip: add context docs to .moxie/context/ (roadmaps, PRDs, etc.)"
789
859
  fi
790
- echo " 3. Run: moxie start (background)"
791
- echo " Or: moxie run (foreground)"
860
+ echo " 3. Run: moxie start (background)"
861
+ echo " Or: moxie run (foreground)"
792
862
  }
793
863
 
794
864
  _init_ledger() {
@@ -1125,6 +1195,7 @@ _print_run_banner() {
1125
1195
  printf "║ Agents: %-51s║\n" "$(IFS=', '; echo "${AGENT_NAMES[*]}")"
1126
1196
  echo "║ Quorum: unanimous (all agents must sign off) ║"
1127
1197
  echo "║ Rotation: randomized per phase ║"
1198
+ echo "║ Swap models: moxie agents swap ║"
1128
1199
  echo "╚══════════════════════════════════════════════════════════════╝"
1129
1200
  echo ""
1130
1201
  echo "Started: $(date)"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zachjxyz/moxie",
3
- "version": "0.4.6",
3
+ "version": "0.5.0",
4
4
  "description": "Run multiple AI coding agents through spec-driven phases with quorum convergence. Supports CLI agents (Claude, Codex, Qwen, Aider, Goose, Amp, Cline, Roo) and Vercel AI Gateway models.",
5
5
  "bin": {
6
6
  "moxie": "bin/moxie"