@@ -389,3 +389,178 @@ def list_config():
389389# Add list as shorthand commands
390390config_app .command (name = "ls" , hidden = True )(list_config )
391391config_app .command (name = "l" , hidden = True )(list_config )
392+
393+
394+ @config_app .command ("codex-profile" , short_help = "Create/update a Codex profile in ~/.codex/config.toml" )
395+ def codex_profile (
396+ config : Optional [str ] = CONFIG_FILE_OPTION ,
397+ name : Optional [str ] = typer .Option (
398+ None , "--name" , "-n" , help = "Profile name to create/update (default: model name)"
399+ ),
400+ reasoning_effort : str = typer .Option (
401+ "low" , "--reasoning-effort" , help = "profiles.<name>.model_reasoning_effort"
402+ ),
403+ ):
404+ """Interactively select a provider endpoint + model, then write a Codex profile."""
405+ from pathlib import Path
406+
407+ from code_assistant_manager .config import ConfigManager
408+ from code_assistant_manager .endpoints import EndpointManager
409+ from code_assistant_manager .menu .base import Colors
410+ from code_assistant_manager .menu .model_selector import ModelSelector
411+
412+ try :
413+ cm = ConfigManager (config )
414+ cm .load_env_file ()
415+ is_valid , errors = cm .validate_config ()
416+ if not is_valid :
417+ typer .echo (f"{ Colors .RED } ✗ Configuration validation failed:{ Colors .RESET } " )
418+ for err in errors :
419+ typer .echo (f" - { err } " )
420+ raise typer .Exit (1 )
421+
422+ em = EndpointManager (cm )
423+
424+ ok , endpoint_name = em .select_endpoint ("codex" )
425+ if not ok or not endpoint_name :
426+ raise typer .Exit (0 )
427+
428+ ok , endpoint_config = em .get_endpoint_config (endpoint_name )
429+ if not ok or not endpoint_config :
430+ raise typer .Exit (1 )
431+
432+ ok , models = em .fetch_models (endpoint_name , endpoint_config )
433+ if not ok or not models :
434+ raise typer .Exit (1 )
435+
436+ ok , model = ModelSelector .select_model_with_endpoint_info (
437+ models , endpoint_name , endpoint_config , "model" , "codex"
438+ )
439+ if not ok or not model :
440+ raise typer .Exit (0 )
441+
442+ profile_name = name or model
443+ provider_key = endpoint_name
444+ env_key = cm .get_endpoint_config (endpoint_name ).get ("api_key_env" ) or "OPENAI_API_KEY"
445+
446+ from code_assistant_manager .tools .config_writers .codex import upsert_codex_profile
447+
448+ config_path = Path .home () / ".codex" / "config.toml"
449+ try :
450+ result = upsert_codex_profile (
451+ config_path = config_path ,
452+ provider = provider_key ,
453+ base_url = endpoint_config .get ("endpoint" , "" ),
454+ env_key = env_key ,
455+ profile = profile_name ,
456+ model = model ,
457+ reasoning_effort = reasoning_effort ,
458+ project_path = Path .cwd ().resolve (),
459+ )
460+ except Exception as e :
461+ typer .echo (f"{ Colors .RED } ✗ Failed to write { config_path } : { e } { Colors .RESET } " )
462+ raise typer .Exit (1 )
463+
464+ if result .get ("changed" ):
465+ typer .echo (f"{ Colors .GREEN } ✓ Wrote Codex profile '{ profile_name } '{ Colors .RESET } " )
466+ else :
467+ typer .echo (f"{ Colors .GREEN } ✓ Codex profile already up to date: '{ profile_name } '{ Colors .RESET } " )
468+ typer .echo (f" Config: { config_path } " )
469+ typer .echo (f" Run: codex -p { profile_name } " )
470+
471+ except typer .Exit :
472+ raise
473+ except Exception as e :
474+ typer .echo (f"{ Colors .RED } ✗ Unexpected error: { e } { Colors .RESET } " )
475+ raise typer .Exit (1 )
476+
477+
478+ @config_app .command (
479+ "codex-profiles" ,
480+ short_help = "Create/update Codex profiles for multiple providers from providers.json" ,
481+ )
482+ def codex_profiles (
483+ config : Optional [str ] = CONFIG_FILE_OPTION ,
484+ reasoning_effort : str = typer .Option (
485+ "low" , "--reasoning-effort" , help = "profiles.<name>.model_reasoning_effort"
486+ ),
487+ ):
488+ """Prompt repeatedly to configure provider+model pairs for Codex.
489+
490+ This is Droid-like: keep selecting a provider and a model (or skip), until you cancel.
491+ """
492+ from pathlib import Path
493+
494+ from code_assistant_manager .tools .config_writers .codex import upsert_codex_profile
495+ from code_assistant_manager .config import ConfigManager
496+ from code_assistant_manager .endpoints import EndpointManager
497+ from code_assistant_manager .menu .base import Colors
498+ from code_assistant_manager .menu .model_selector import ModelSelector
499+
500+ cm = ConfigManager (config )
501+ cm .load_env_file ()
502+ is_valid , errors = cm .validate_config ()
503+ if not is_valid :
504+ typer .echo (f"{ Colors .RED } ✗ Configuration validation failed:{ Colors .RESET } " )
505+ for err in errors :
506+ typer .echo (f" - { err } " )
507+ raise typer .Exit (1 )
508+
509+ em = EndpointManager (cm )
510+
511+ endpoints = cm .get_sections (exclude_common = True )
512+ endpoints = [ep for ep in endpoints if em ._is_client_supported (ep , "codex" )]
513+ if not endpoints :
514+ typer .echo (f"{ Colors .RED } ✗ No endpoints configured for codex{ Colors .RESET } " )
515+ raise typer .Exit (1 )
516+
517+ config_path = Path .home () / ".codex" / "config.toml"
518+ changed_any = False
519+ configured = 0
520+
521+ # Prompt providers one by one (like Droid): pick one model per provider (or skip).
522+ for endpoint_name in endpoints :
523+ ok , endpoint_config = em .get_endpoint_config (endpoint_name )
524+ if not ok or not endpoint_config :
525+ continue
526+
527+ ok , models = em .fetch_models (
528+ endpoint_name , endpoint_config , use_cache_if_available = False
529+ )
530+ if not ok or not models :
531+ continue
532+
533+ ok , model = ModelSelector .select_model_with_endpoint_info (
534+ models , endpoint_name , endpoint_config , "model" , "codex"
535+ )
536+ if not ok or not model :
537+ typer .echo (f"Skipped { endpoint_name } " )
538+ continue
539+
540+ profile_name = model
541+ env_key = cm .get_endpoint_config (endpoint_name ).get ("api_key_env" ) or "OPENAI_API_KEY"
542+
543+ result = upsert_codex_profile (
544+ config_path = config_path ,
545+ provider = endpoint_name ,
546+ base_url = endpoint_config .get ("endpoint" , "" ),
547+ env_key = env_key ,
548+ profile = profile_name ,
549+ model = model ,
550+ reasoning_effort = reasoning_effort ,
551+ project_path = Path .cwd ().resolve (),
552+ )
553+
554+ changed_any = changed_any or bool (result .get ("changed" ))
555+ configured += 1
556+
557+ if configured == 0 :
558+ typer .echo (f"{ Colors .YELLOW } ! No profiles configured{ Colors .RESET } " )
559+ raise typer .Exit (0 )
560+
561+ if changed_any :
562+ typer .echo (f"{ Colors .GREEN } ✓ Updated Codex profiles ({ configured } ){ Colors .RESET } " )
563+ else :
564+ typer .echo (f"{ Colors .GREEN } ✓ Codex config already up to date ({ configured } ){ Colors .RESET } " )
565+
566+ typer .echo (f" Config: { config_path } " )
0 commit comments