@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 +3 -2
- package/lib/agents.sh +405 -8
- package/lib/phases.sh +268 -197
- package/package.json +1 -1
package/bin/moxie
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
set -euo pipefail
|
|
19
19
|
|
|
20
|
-
MOXIE_VERSION="0.
|
|
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
|
-
|
|
647
|
-
print(f' {marker} {m[\"id\"]}')
|
|
1046
|
+
print(f' {m[\"id\"]}')
|
|
648
1047
|
print()
|
|
649
1048
|
|
|
650
|
-
print('
|
|
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
|
|
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
|
-
#
|
|
222
|
-
local
|
|
223
|
-
local
|
|
224
|
-
local
|
|
225
|
-
local
|
|
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
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
#
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
local
|
|
272
|
-
local
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
#
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
275
|
+
fi
|
|
291
276
|
|
|
292
|
-
#
|
|
293
|
-
|
|
294
|
-
|
|
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 +
|
|
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
|
-
|
|
301
|
-
|
|
317
|
+
local gw_filtered=()
|
|
318
|
+
_build_gw_filtered
|
|
302
319
|
|
|
303
|
-
|
|
320
|
+
_sa_render() {
|
|
321
|
+
# Count total selected
|
|
304
322
|
local sel_count=0
|
|
305
|
-
for (( i = 0; i <
|
|
306
|
-
[ "${
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
[ "$
|
|
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
|
-
[ "${
|
|
322
|
-
|
|
323
|
-
|
|
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
|
|
382
|
+
printf "\\r\\033[K\\n" >&2
|
|
326
383
|
fi
|
|
327
|
-
|
|
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
|
-
[ "$
|
|
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 ·
|
|
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)
|
|
408
|
+
printf "Select agents (at least 2):\\n\\n" >&2
|
|
343
409
|
printf "\\033[?25l" >&2
|
|
344
|
-
|
|
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
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
383
|
-
|
|
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
|
|
387
|
-
|
|
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 <
|
|
390
|
-
[ "${
|
|
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 [ "$
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
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" "$
|
|
467
|
-
|
|
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
|
-
#
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
|
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
|
|
786
|
-
echo " 2.
|
|
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
|
|
791
|
-
echo " Or: moxie run
|
|
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.
|
|
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"
|