@zachjxyz/moxie 0.4.5 → 0.4.9

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.5"
20
+ MOXIE_VERSION="0.4.9"
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
@@ -246,29 +246,18 @@ _select_agents() {
246
246
  [ "$len" -gt "$max_label_len" ] && max_label_len=$len
247
247
  done
248
248
 
249
- # Separator
249
+ # Separator + gateway search
250
250
  item_labels+=("--- Vercel AI Gateway ---")
251
251
  item_meta+=("")
252
252
  item_types+=("separator")
253
253
  item_sources+=("-1")
254
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")
262
- local len=${#label}
263
- [ "$len" -gt "$max_label_len" ] && max_label_len=$len
264
- done
265
-
266
- # Custom model entry point
267
- item_labels+=("+ Add custom model...")
268
- item_meta+=("type provider/model-name")
255
+ item_labels+=("+ Search gateway models...")
256
+ item_meta+=("browse all available models")
269
257
  item_types+=("custom_add")
270
258
  item_sources+=("-1")
271
- local len=${#item_labels[-1]}
259
+ local _custom_label="+ Search gateway models..."
260
+ local len=${#_custom_label}
272
261
  [ "$len" -gt "$max_label_len" ] && max_label_len=$len
273
262
 
274
263
  # Track custom models added during this session
@@ -335,10 +324,10 @@ _select_agents() {
335
324
  printf "\\r\\033[K %s\\033[2m%s\\033[0m\\n" "$submit_marker" "Submit (need at least 2)" >&2
336
325
  fi
337
326
 
338
- printf "\\r\\033[K\\n\\033[K \\033[2m↑↓ navigate · space toggle · enter submit/add · 'moxie models' to browse\\033[0m" >&2
327
+ printf "\\r\\033[K\\n\\033[K \\033[2m↑↓ navigate · space toggle · enter submit/search\\033[0m" >&2
339
328
  }
340
329
 
341
- printf "Select agents (at least 2). CLI agents pre-selected, gateway models available below:\\n\\n" >&2
330
+ printf "Select agents (at least 2). Search gateway models to add more:\\n\\n" >&2
342
331
  printf "\\033[?25l" >&2
343
332
  _agent_render
344
333
 
@@ -390,70 +379,297 @@ _select_agents() {
390
379
  done
391
380
  [ "$sel_count" -ge 2 ] && break
392
381
  elif [ "${item_types[$cursor]}" = "custom_add" ]; then
393
- # Prompt for custom model string
382
+ # Live search picker fetch models from gateway, filter interactively
383
+ stty "$old_stty" < /dev/tty 2>/dev/null
394
384
  printf "\\033[?25h" >&2
395
- stty echo icanon < /dev/tty 2>/dev/null
396
- printf "\\n\\r\\033[K Model ID (provider/model-name): " >&2
397
- local custom_model=""
398
- read -r custom_model < /dev/tty
399
- stty -echo -icanon min 1 < /dev/tty 2>/dev/null
400
- printf "\\033[?25l" >&2
401
385
 
402
- if [ -n "$custom_model" ] && [[ "$custom_model" == */* ]]; then
403
- # Derive a short name from the model ID: provider-modelbase
404
- local custom_provider="${custom_model%%/*}"
405
- local custom_name_part="${custom_model#*/}"
406
- local custom_slug="${custom_provider}-${custom_name_part}"
407
- # Sanitize slug for TOML key
408
- custom_slug=$(echo "$custom_slug" | tr '[:upper:]' '[:lower:]' | tr ' .' '-' | tr -cd 'a-z0-9-')
409
- custom_slug="${custom_slug}-gw"
410
-
411
- # Insert before the custom_add row (which is at current cursor)
412
- local insert_at=$cursor
413
-
414
- # Shift arrays to insert new item
415
- local new_labels=() new_meta=() new_types=() new_sources=() new_selected=()
416
- for (( i = 0; i < count; i++ )); do
417
- if [ "$i" -eq "$insert_at" ]; then
418
- new_labels+=("$custom_name_part")
419
- new_meta+=("$custom_model")
420
- new_types+=("custom_gateway")
421
- new_sources+=("${#custom_gateway_names[@]}")
422
- new_selected+=(1)
386
+ # Ensure we have an API key
387
+ local _gw_key=""
388
+ if gateway_has_key "vercel-ai-gateway"; then
389
+ _gw_key=$(gateway_get_key "vercel-ai-gateway" 2>/dev/null) || true
390
+ fi
391
+ if [ -z "$_gw_key" ]; then
392
+ printf "\\n Gateway API key needed to search models.\\n\\n" >&2
393
+ gateway_store_key "vercel-ai-gateway" || {
394
+ printf " \\033[31mFailed to store key. Skipping search.\\033[0m\\n" >&2
395
+ sleep 1
396
+ stty -echo -icanon min 1 < /dev/tty 2>/dev/null
397
+ printf "\\033[?25l" >&2
398
+ printf "\\033[%dA" "$jump_back" >&2
399
+ _agent_render
400
+ continue
401
+ }
402
+ _gw_key=$(gateway_get_key "vercel-ai-gateway" 2>/dev/null) || true
403
+ fi
404
+
405
+ # Fetch model list
406
+ printf "\\n Fetching models from AI Gateway..." >&2
407
+ local _models_json=""
408
+ _models_json=$(GATEWAY_API_KEY="$_gw_key" node "$MOXIE_LIB/gateway-models.mjs" "https://ai-gateway.vercel.sh" "" 2>/dev/null) || true
409
+ printf "\\r\\033[K" >&2
410
+
411
+ if [ -z "$_models_json" ]; then
412
+ printf " \\033[31mFailed to fetch models. Check your API key or network.\\033[0m\\n" >&2
413
+ sleep 2
414
+ stty -echo -icanon min 1 < /dev/tty 2>/dev/null
415
+ printf "\\033[?25l" >&2
416
+ printf "\\033[%dA" "$((jump_back + 2))" >&2
417
+ _agent_render
418
+ continue
419
+ fi
420
+
421
+ # Parse into arrays using python3
422
+ local _all_model_ids=()
423
+ local _all_model_labels=()
424
+ eval "$(python3 -c "
425
+ import json, sys, shlex
426
+ data = json.loads(sys.stdin.read())
427
+ models = data.get('models', [])
428
+ ids = []
429
+ labels = []
430
+ for m in models:
431
+ mid = m['id']
432
+ ids.append(mid)
433
+ labels.append(m.get('name', mid))
434
+ # Output as bash array assignments
435
+ print('_all_model_ids=(' + ' '.join(shlex.quote(x) for x in ids) + ')')
436
+ print('_all_model_labels=(' + ' '.join(shlex.quote(x) for x in labels) + ')')
437
+ " <<< "$_models_json" 2>/dev/null)"
438
+
439
+ if [ ${#_all_model_ids[@]} -eq 0 ]; then
440
+ printf " \\033[31mNo models returned from gateway.\\033[0m\\n" >&2
441
+ sleep 1
442
+ stty -echo -icanon min 1 < /dev/tty 2>/dev/null
443
+ printf "\\033[?25l" >&2
444
+ printf "\\033[%dA" "$((jump_back + 2))" >&2
445
+ _agent_render
446
+ continue
447
+ fi
448
+
449
+ # Build set of already-selected model IDs
450
+ local _existing_ids=""
451
+ for _eid in "${custom_gateway_models[@]}"; do
452
+ _existing_ids="${_existing_ids}|${_eid}"
453
+ done
454
+
455
+ # Interactive search sub-TUI
456
+ local _search=""
457
+ local _scursor=0
458
+ local _sselected=()
459
+ for (( _si = 0; _si < ${#_all_model_ids[@]}; _si++ )); do
460
+ _sselected+=(0)
461
+ done
462
+
463
+ local _max_visible=15
464
+
465
+ _search_render() {
466
+ # Build filtered index list
467
+ local _filtered=()
468
+ local _lc_search
469
+ _lc_search=$(echo "$_search" | tr '[:upper:]' '[:lower:]')
470
+ for (( _si = 0; _si < ${#_all_model_ids[@]}; _si++ )); do
471
+ if [ -z "$_search" ]; then
472
+ _filtered+=("$_si")
473
+ else
474
+ local _lc_id
475
+ _lc_id=$(echo "${_all_model_ids[$_si]}" | tr '[:upper:]' '[:lower:]')
476
+ if [[ "$_lc_id" == *"$_lc_search"* ]]; then
477
+ _filtered+=("$_si")
478
+ fi
423
479
  fi
424
- new_labels+=("${item_labels[$i]}")
425
- new_meta+=("${item_meta[$i]}")
426
- new_types+=("${item_types[$i]}")
427
- new_sources+=("${item_sources[$i]}")
428
- new_selected+=("${selected[$i]}")
429
480
  done
430
481
 
431
- item_labels=("${new_labels[@]}")
432
- item_meta=("${new_meta[@]}")
433
- item_types=("${new_types[@]}")
434
- item_sources=("${new_sources[@]}")
435
- selected=("${new_selected[@]}")
482
+ local _fcount=${#_filtered[@]}
436
483
 
437
- custom_gateway_names+=("$custom_slug")
438
- custom_gateway_models+=("$custom_model")
484
+ # Clamp cursor
485
+ [ "$_scursor" -ge "$_fcount" ] && _scursor=$(( _fcount > 0 ? _fcount - 1 : 0 ))
486
+ [ "$_scursor" -lt 0 ] && _scursor=0
439
487
 
440
- count=${#item_labels[@]}
441
- jump_back=$(( count + 3 ))
488
+ # Visible window
489
+ local _vstart=0
490
+ if [ "$_scursor" -ge "$_max_visible" ]; then
491
+ _vstart=$(( _scursor - _max_visible + 1 ))
492
+ fi
493
+ local _vend=$(( _vstart + _max_visible ))
494
+ [ "$_vend" -gt "$_fcount" ] && _vend=$_fcount
442
495
 
443
- # Update max_label_len if needed
444
- local len=${#custom_name_part}
445
- [ "$len" -gt "$max_label_len" ] && max_label_len=$len
446
- sep=""
447
- for (( i = 0; i < max_label_len + 40; i++ )); do sep="${sep}-"; done
448
- else
449
- # Invalid — erase the prompt line
450
- if [ -n "$custom_model" ]; then
451
- printf "\\r\\033[K \\033[31mInvalid format. Use provider/model-name (e.g. anthropic/claude-sonnet-4-6)\\033[0m" >&2
452
- sleep 1
496
+ # Count selected
497
+ local _sel_count=0
498
+ for (( _si = 0; _si < ${#_sselected[@]}; _si++ )); do
499
+ [ "${_sselected[$_si]}" = "1" ] && _sel_count=$(( _sel_count + 1 ))
500
+ done
501
+
502
+ # Render
503
+ printf "\\r\\033[K \\033[1mSearch gateway models\\033[0m (%d available, %d selected)\\n" "$_fcount" "$_sel_count" >&2
504
+ printf "\\r\\033[K Search: \\033[36m%s\\033[0m\\033[2m|\\033[0m\\n\\n" "$_search" >&2
505
+
506
+ local _rendered=0
507
+ for (( _vi = _vstart; _vi < _vend; _vi++ )); do
508
+ local _ri=${_filtered[$_vi]}
509
+ local _mid="${_all_model_ids[$_ri]}"
510
+ local _marker=" "
511
+ [ "$_vi" -eq "$_scursor" ] && _marker="> "
512
+ local _check="[ ]"
513
+ [ "${_sselected[$_ri]}" = "1" ] && _check="[x]"
514
+ local _exists_marker=""
515
+ if [[ "$_existing_ids" == *"|${_mid}"* ]]; then
516
+ _exists_marker=" \\033[2m(added)\\033[0m"
517
+ fi
518
+ printf "\\r\\033[K %s%s %s%b\\n" "$_marker" "$_check" "$_mid" "$_exists_marker" >&2
519
+ _rendered=$(( _rendered + 1 ))
520
+ done
521
+
522
+ # Pad remaining lines
523
+ while [ "$_rendered" -lt "$_max_visible" ]; do
524
+ printf "\\r\\033[K\\n" >&2
525
+ _rendered=$(( _rendered + 1 ))
526
+ done
527
+
528
+ if [ "$_vstart" -gt 0 ] || [ "$_vend" -lt "$_fcount" ]; then
529
+ printf "\\r\\033[K \\033[2m(%d-%d of %d · scroll for more)\\033[0m\\n" "$(( _vstart + 1 ))" "$_vend" "$_fcount" >&2
530
+ else
531
+ printf "\\r\\033[K\\n" >&2
453
532
  fi
533
+
534
+ printf "\\r\\033[K \\033[2m↑↓ navigate · space select · enter confirm · esc cancel · type to filter\\033[0m" >&2
535
+ }
536
+
537
+ # Total lines rendered by _search_render: 3 header + _max_visible + 1 scroll + 1 hint = _max_visible + 5
538
+ local _search_lines=$(( _max_visible + 5 ))
539
+
540
+ printf "\\033[?25l" >&2
541
+ stty -echo -icanon min 1 < /dev/tty 2>/dev/null
542
+
543
+ printf "\\n" >&2
544
+ _search_render
545
+
546
+ local _search_done=0
547
+ local _search_cancelled=0
548
+ while [ "$_search_done" = "0" ]; do
549
+ local _skey
550
+ _skey=$(dd bs=1 count=1 2>/dev/null < /dev/tty) || break
551
+
552
+ if [ "$_skey" = $'\033' ]; then
553
+ local _sbracket _sdir
554
+ _sbracket=$(dd bs=1 count=1 2>/dev/null < /dev/tty) || true
555
+ _sdir=$(dd bs=1 count=1 2>/dev/null < /dev/tty) || true
556
+ if [ "$_sbracket" = "[" ]; then
557
+ case "$_sdir" in
558
+ A) _scursor=$(( _scursor - 1 )); [ "$_scursor" -lt 0 ] && _scursor=0 ;;
559
+ B) _scursor=$(( _scursor + 1 )) ;;
560
+ esac
561
+ else
562
+ # Bare escape — cancel
563
+ _search_cancelled=1
564
+ _search_done=1
565
+ fi
566
+ elif [ "$_skey" = " " ]; then
567
+ # Toggle selection at cursor
568
+ local _filtered_for_toggle=()
569
+ local _lc_s2
570
+ _lc_s2=$(echo "$_search" | tr '[:upper:]' '[:lower:]')
571
+ for (( _si = 0; _si < ${#_all_model_ids[@]}; _si++ )); do
572
+ if [ -z "$_search" ]; then
573
+ _filtered_for_toggle+=("$_si")
574
+ else
575
+ local _lc2
576
+ _lc2=$(echo "${_all_model_ids[$_si]}" | tr '[:upper:]' '[:lower:]')
577
+ [[ "$_lc2" == *"$_lc_s2"* ]] && _filtered_for_toggle+=("$_si")
578
+ fi
579
+ done
580
+ if [ "$_scursor" -lt "${#_filtered_for_toggle[@]}" ]; then
581
+ local _tidx=${_filtered_for_toggle[$_scursor]}
582
+ [ "${_sselected[$_tidx]}" = "0" ] && _sselected[$_tidx]=1 || _sselected[$_tidx]=0
583
+ fi
584
+ elif [ "$_skey" = "" ]; then
585
+ # Confirm
586
+ _search_done=1
587
+ elif [ "$_skey" = $'\177' ] || [ "$_skey" = $'\010' ]; then
588
+ # Backspace
589
+ if [ -n "$_search" ]; then
590
+ _search="${_search%?}"
591
+ _scursor=0
592
+ fi
593
+ else
594
+ # Printable character — append to search
595
+ if [[ "$_skey" =~ [[:print:]] ]]; then
596
+ _search="${_search}${_skey}"
597
+ _scursor=0
598
+ fi
599
+ fi
600
+
601
+ printf "\\033[%dA" "$_search_lines" >&2
602
+ _search_render
603
+ done
604
+
605
+ # Clear the search sub-TUI
606
+ printf "\\033[%dA" "$_search_lines" >&2
607
+ for (( _ci = 0; _ci <= _search_lines; _ci++ )); do
608
+ printf "\\r\\033[K\\n" >&2
609
+ done
610
+ printf "\\033[%dA" "$(( _search_lines + 1 ))" >&2
611
+
612
+ # Insert selected models into the main list
613
+ if [ "$_search_cancelled" = "0" ]; then
614
+ for (( _si = 0; _si < ${#_sselected[@]}; _si++ )); do
615
+ [ "${_sselected[$_si]}" != "1" ] && continue
616
+ local _new_model="${_all_model_ids[$_si]}"
617
+ local _new_label="${_all_model_labels[$_si]}"
618
+
619
+ # Skip if already in list
620
+ if [[ "$_existing_ids" == *"|${_new_model}"* ]]; then
621
+ continue
622
+ fi
623
+ _existing_ids="${_existing_ids}|${_new_model}"
624
+
625
+ # Derive slug for TOML key
626
+ local _cprov="${_new_model%%/*}"
627
+ local _cname="${_new_model#*/}"
628
+ local _cslug
629
+ _cslug=$(echo "${_cprov}-${_cname}" | tr '[:upper:]' '[:lower:]' | tr ' .' '-' | tr -cd 'a-z0-9-')
630
+ _cslug="${_cslug}-gw"
631
+
632
+ # Insert before the custom_add row
633
+ local _ins=$cursor
634
+ local new_labels=() new_meta=() new_types=() new_sources=() new_selected=()
635
+ for (( _ii = 0; _ii < count; _ii++ )); do
636
+ if [ "$_ii" -eq "$_ins" ]; then
637
+ new_labels+=("$_new_label")
638
+ new_meta+=("$_new_model")
639
+ new_types+=("custom_gateway")
640
+ new_sources+=("${#custom_gateway_names[@]}")
641
+ new_selected+=(1)
642
+ fi
643
+ new_labels+=("${item_labels[$_ii]}")
644
+ new_meta+=("${item_meta[$_ii]}")
645
+ new_types+=("${item_types[$_ii]}")
646
+ new_sources+=("${item_sources[$_ii]}")
647
+ new_selected+=("${selected[$_ii]}")
648
+ done
649
+
650
+ item_labels=("${new_labels[@]}")
651
+ item_meta=("${new_meta[@]}")
652
+ item_types=("${new_types[@]}")
653
+ item_sources=("${new_sources[@]}")
654
+ selected=("${new_selected[@]}")
655
+
656
+ custom_gateway_names+=("$_cslug")
657
+ custom_gateway_models+=("$_new_model")
658
+
659
+ count=${#item_labels[@]}
660
+ cursor=$(( cursor + 1 ))
661
+
662
+ local _ll=${#_new_label}
663
+ [ "$_ll" -gt "$max_label_len" ] && max_label_len=$_ll
664
+ done
665
+
666
+ jump_back=$(( count + 3 ))
667
+ sep=""
668
+ for (( _si = 0; _si < max_label_len + 40; _si++ )); do sep="${sep}-"; done
454
669
  fi
455
- # Erase the prompt lines and re-render
456
- printf "\\033[2A" >&2
670
+
671
+ stty -echo -icanon min 1 < /dev/tty 2>/dev/null
672
+ printf "\\033[?25l" >&2
457
673
  elif [ "${item_types[$cursor]}" != "separator" ]; then
458
674
  [ "${selected[$cursor]}" = "0" ] && selected[$cursor]=1 || selected[$cursor]=0
459
675
  fi
@@ -781,13 +997,13 @@ SEED
781
997
 
782
998
  echo ""
783
999
  echo "Next steps:"
784
- echo " 1. Run: moxie doctor (verify dependencies and environment)"
785
- echo " 2. Review .moxie/config.toml (agent commands, timeouts)"
1000
+ echo " 1. Run: moxie doctor (verify dependencies and environment)"
1001
+ echo " 2. Run: moxie agents swap (add or change gateway models anytime)"
786
1002
  if [ ! -d "$MOXIE_DIR/context" ] || [ -z "$(ls -A "$MOXIE_DIR/context" 2>/dev/null)" ]; then
787
1003
  echo " Tip: add context docs to .moxie/context/ (roadmaps, PRDs, etc.)"
788
1004
  fi
789
- echo " 3. Run: moxie start (background)"
790
- echo " Or: moxie run (foreground)"
1005
+ echo " 3. Run: moxie start (background)"
1006
+ echo " Or: moxie run (foreground)"
791
1007
  }
792
1008
 
793
1009
  _init_ledger() {
@@ -1124,6 +1340,7 @@ _print_run_banner() {
1124
1340
  printf "║ Agents: %-51s║\n" "$(IFS=', '; echo "${AGENT_NAMES[*]}")"
1125
1341
  echo "║ Quorum: unanimous (all agents must sign off) ║"
1126
1342
  echo "║ Rotation: randomized per phase ║"
1343
+ echo "║ Swap models: moxie agents swap ║"
1127
1344
  echo "╚══════════════════════════════════════════════════════════════╝"
1128
1345
  echo ""
1129
1346
  echo "Started: $(date)"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zachjxyz/moxie",
3
- "version": "0.4.5",
3
+ "version": "0.4.9",
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"