mirror of
https://github.com/katanemo/plano.git
synced 2026-05-21 13:55:15 +02:00
feat: enhance CLI agent support with Codex and OpenCode integration
This commit is contained in:
parent
5d35a3ae18
commit
fe43cf5ecb
2 changed files with 117 additions and 40 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue