feat: enhance CLI agent support with Codex and OpenCode integration

This commit is contained in:
Musa 2026-02-25 10:16:32 -08:00
parent 5d35a3ae18
commit fe43cf5ecb
No known key found for this signature in database
2 changed files with 117 additions and 40 deletions

View file

@ -144,17 +144,32 @@ def stop_docker_container(service=PLANO_DOCKER_NAME):
log.info(f"Failed to shut down services: {str(e)}") log.info(f"Failed to shut down services: {str(e)}")
def start_cli_agent(plano_config_file=None, settings_json="{}"): def start_cli_agent(plano_config_file=None, settings_json="{}", agent_type="claude"):
"""Start a CLI client connected to Plano.""" """Start a CLI client connected to Plano."""
with open(plano_config_file, "r") as file: with open(plano_config_file, "r") as file:
plano_config = file.read() plano_config = file.read()
plano_config_yaml = yaml.safe_load(plano_config) plano_config_yaml = yaml.safe_load(plano_config)
# Get egress listener configuration # Get egress listener configuration (supports both legacy dict and list formats)
egress_config = plano_config_yaml.get("listeners", {}).get("egress_traffic", {}) host = "127.0.0.1"
host = egress_config.get("host", "127.0.0.1") port = 12000
port = egress_config.get("port", 12000) listeners = plano_config_yaml.get("listeners")
if isinstance(listeners, dict):
egress_config = listeners.get("egress_traffic", {})
host = egress_config.get("host", host)
port = egress_config.get("port", port)
elif isinstance(listeners, list):
model_listener = next(
(
listener
for listener in listeners
if listener.get("type") in ("model", "model_listener")
),
{},
)
host = model_listener.get("host", host)
port = model_listener.get("port", port)
# Parse additional settings from command line # Parse additional settings from command line
try: try:
@ -167,25 +182,34 @@ def start_cli_agent(plano_config_file=None, settings_json="{}"):
env = os.environ.copy() env = os.environ.copy()
env.update( env.update(
{ {
"ANTHROPIC_AUTH_TOKEN": "test", # Use test token for plano
"ANTHROPIC_API_KEY": "",
"ANTHROPIC_BASE_URL": f"http://{host}:{port}",
"NO_PROXY": host, "NO_PROXY": host,
"DISABLE_TELEMETRY": "true", "DISABLE_TELEMETRY": "true",
"DISABLE_COST_WARNINGS": "true",
"API_TIMEOUT_MS": "600000", "API_TIMEOUT_MS": "600000",
} }
) )
# Set ANTHROPIC_SMALL_FAST_MODEL from additional_settings or model alias model_aliases = plano_config_yaml.get("model_aliases", {})
if "ANTHROPIC_SMALL_FAST_MODEL" in additional_settings: command_path = None
env["ANTHROPIC_SMALL_FAST_MODEL"] = additional_settings[ command_args = []
"ANTHROPIC_SMALL_FAST_MODEL" handled_settings = {"NON_INTERACTIVE_MODE"}
]
else: if agent_type == "claude":
# Check if arch.claude.code.small.fast alias exists in model_aliases env.update(
model_aliases = plano_config_yaml.get("model_aliases", {}) {
if "arch.claude.code.small.fast" in model_aliases: "ANTHROPIC_AUTH_TOKEN": "test", # Use test token for plano
"ANTHROPIC_API_KEY": "",
"ANTHROPIC_BASE_URL": f"http://{host}:{port}",
"DISABLE_COST_WARNINGS": "true",
}
)
command_path = "claude"
# Set ANTHROPIC_SMALL_FAST_MODEL from additional_settings or model alias
if "ANTHROPIC_SMALL_FAST_MODEL" in additional_settings:
env["ANTHROPIC_SMALL_FAST_MODEL"] = additional_settings[
"ANTHROPIC_SMALL_FAST_MODEL"
]
elif "arch.claude.code.small.fast" in model_aliases:
env["ANTHROPIC_SMALL_FAST_MODEL"] = "arch.claude.code.small.fast" env["ANTHROPIC_SMALL_FAST_MODEL"] = "arch.claude.code.small.fast"
else: else:
log.info( log.info(
@ -193,6 +217,56 @@ def start_cli_agent(plano_config_file=None, settings_json="{}"):
) )
log.info("Or provide ANTHROPIC_SMALL_FAST_MODEL in --settings JSON") log.info("Or provide ANTHROPIC_SMALL_FAST_MODEL in --settings JSON")
handled_settings.add("ANTHROPIC_SMALL_FAST_MODEL")
elif agent_type == "codex":
env.update(
{
# Codex uses OpenAI-compatible auth/base URL when routing through Plano.
"OPENAI_API_KEY": env.get("OPENAI_API_KEY", "test"),
"OPENAI_BASE_URL": f"http://{host}:{port}/v1",
}
)
command_path = "codex"
codex_model = additional_settings.get("CODEX_MODEL")
if codex_model is None and "arch.codex.default" in model_aliases:
# Codex expects known model metadata. Resolve alias to concrete target by default
# to avoid metadata fallback warnings for custom alias names.
codex_model = model_aliases["arch.codex.default"].get(
"target", "arch.codex.default"
)
if codex_model:
command_args.extend(["-m", codex_model])
handled_settings.add("CODEX_MODEL")
elif agent_type == "opencode":
env.update(
{
# OpenCode uses OpenAI-compatible auth/base URL when routing through Plano.
"OPENAI_API_KEY": env.get("OPENAI_API_KEY", "test"),
"OPENAI_BASE_URL": f"http://{host}:{port}/v1",
}
)
command_path = "opencode"
opencode_model = additional_settings.get("OPENCODE_MODEL")
if opencode_model is None and "arch.opencode.default" in model_aliases:
opencode_model = model_aliases["arch.opencode.default"].get(
"target", "arch.opencode.default"
)
if opencode_model:
# Set both generic and client-specific model env vars for compatibility.
env["OPENAI_MODEL"] = opencode_model
env["OPENCODE_MODEL"] = opencode_model
handled_settings.add("OPENCODE_MODEL")
else:
raise ValueError(
f"Unsupported cli-agent type '{agent_type}'. Supported values: claude, codex, opencode"
)
# Non-interactive mode configuration from additional_settings only # Non-interactive mode configuration from additional_settings only
if additional_settings.get("NON_INTERACTIVE_MODE", False): if additional_settings.get("NON_INTERACTIVE_MODE", False):
env.update( env.update(
@ -204,31 +278,32 @@ def start_cli_agent(plano_config_file=None, settings_json="{}"):
} }
) )
# Build claude command arguments # Add passthrough settings for supported agents.
claude_args = []
# Add settings if provided, excluding those already handled as environment variables
if additional_settings: if additional_settings:
# Filter out settings that are already processed as environment variables passthrough_settings = {
claude_settings = { k: v for k, v in additional_settings.items() if k not in handled_settings
k: v
for k, v in additional_settings.items()
if k not in ["ANTHROPIC_SMALL_FAST_MODEL", "NON_INTERACTIVE_MODE"]
} }
if claude_settings: if agent_type == "claude" and passthrough_settings:
claude_args.append(f"--settings={json.dumps(claude_settings)}") command_args.append(f"--settings={json.dumps(passthrough_settings)}")
# Use claude from PATH log.info(f"Connecting {agent_type} CLI agent to Plano at {host}:{port}")
claude_path = "claude"
log.info(f"Connecting Claude Code Agent to Plano at {host}:{port}")
try: try:
subprocess.run([claude_path] + claude_args, env=env, check=True) subprocess.run([command_path] + command_args, env=env, check=True)
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
log.error(f"Error starting claude: {e}") log.error(f"Error starting {agent_type}: {e}")
sys.exit(1) sys.exit(1)
except FileNotFoundError: except FileNotFoundError:
log.error( if agent_type == "claude":
f"{claude_path} not found. Make sure Claude Code is installed: npm install -g @anthropic-ai/claude-code" log.error(
) "claude not found. Make sure Claude Code is installed: npm install -g @anthropic-ai/claude-code"
)
elif agent_type == "codex":
log.error(
"codex not found. Make sure Codex CLI is installed: npm install -g @openai/codex"
)
else:
log.error(
"opencode not found. Make sure OpenCode CLI is installed and available in PATH"
)
sys.exit(1) sys.exit(1)

View file

@ -388,7 +388,9 @@ def logs(debug, follow):
@click.command() @click.command()
@click.argument("type", type=click.Choice(["claude"]), required=True) @click.argument(
"type", type=click.Choice(["claude", "codex", "opencode"]), required=True
)
@click.argument("file", required=False) # Optional file argument @click.argument("file", required=False) # Optional file argument
@click.option( @click.option(
"--path", default=".", help="Path to the directory containing plano_config.yaml" "--path", default=".", help="Path to the directory containing plano_config.yaml"
@ -401,7 +403,7 @@ def logs(debug, follow):
def cli_agent(type, file, path, settings): def cli_agent(type, file, path, settings):
"""Start a CLI agent connected to Plano. """Start a CLI agent connected to Plano.
CLI_AGENT: The type of CLI agent to start (currently only 'claude' is supported) CLI_AGENT: The type of CLI agent to start (claude, codex, opencode)
""" """
# Check if plano docker container is running # Check if plano docker container is running
@ -418,7 +420,7 @@ def cli_agent(type, file, path, settings):
sys.exit(1) sys.exit(1)
try: try:
start_cli_agent(plano_config_file, settings) start_cli_agent(plano_config_file, settings, type)
except SystemExit: except SystemExit:
# Re-raise SystemExit to preserve exit codes # Re-raise SystemExit to preserve exit codes
raise raise