mirror of
https://github.com/katanemo/plano.git
synced 2026-05-21 13:55:15 +02:00
merge main
This commit is contained in:
commit
692499d910
22 changed files with 1771 additions and 215 deletions
|
|
@ -10,7 +10,6 @@ from planoai.consts import (
|
|||
PLANO_DOCKER_IMAGE,
|
||||
PLANO_DOCKER_NAME,
|
||||
)
|
||||
import subprocess
|
||||
from planoai.docker_cli import (
|
||||
docker_container_status,
|
||||
docker_remove_container,
|
||||
|
|
@ -147,26 +146,48 @@ def stop_docker_container(service=PLANO_DOCKER_NAME):
|
|||
log.info(f"Failed to shut down services: {str(e)}")
|
||||
|
||||
|
||||
def start_cli_agent(plano_config_file=None, settings_json="{}"):
|
||||
"""Start a CLI client connected to Plano."""
|
||||
|
||||
with open(plano_config_file, "r") as file:
|
||||
plano_config = file.read()
|
||||
plano_config_yaml = yaml.safe_load(plano_config)
|
||||
|
||||
# Get egress listener configuration
|
||||
egress_config = plano_config_yaml.get("listeners", {}).get("egress_traffic", {})
|
||||
host = egress_config.get("host", "127.0.0.1")
|
||||
port = egress_config.get("port", 12000)
|
||||
|
||||
# Parse additional settings from command line
|
||||
def _parse_cli_agent_settings(settings_json: str) -> dict:
|
||||
try:
|
||||
additional_settings = json.loads(settings_json) if settings_json else {}
|
||||
return json.loads(settings_json) if settings_json else {}
|
||||
except json.JSONDecodeError:
|
||||
log.error("Settings must be valid JSON")
|
||||
sys.exit(1)
|
||||
|
||||
# Set up environment variables
|
||||
|
||||
def _resolve_cli_agent_endpoint(plano_config_yaml: dict) -> tuple[str, int]:
|
||||
listeners = plano_config_yaml.get("listeners")
|
||||
|
||||
if isinstance(listeners, dict):
|
||||
egress_config = listeners.get("egress_traffic", {})
|
||||
host = egress_config.get("host") or egress_config.get("address") or "0.0.0.0"
|
||||
port = egress_config.get("port", 12000)
|
||||
return host, port
|
||||
|
||||
if isinstance(listeners, list):
|
||||
for listener in listeners:
|
||||
if listener.get("type") in ["model", "model_listener"]:
|
||||
host = listener.get("host") or listener.get("address") or "0.0.0.0"
|
||||
port = listener.get("port", 12000)
|
||||
return host, port
|
||||
|
||||
return "0.0.0.0", 12000
|
||||
|
||||
|
||||
def _apply_non_interactive_env(env: dict, additional_settings: dict) -> None:
|
||||
if additional_settings.get("NON_INTERACTIVE_MODE", False):
|
||||
env.update(
|
||||
{
|
||||
"CI": "true",
|
||||
"FORCE_COLOR": "0",
|
||||
"NODE_NO_READLINE": "1",
|
||||
"TERM": "dumb",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _start_claude_cli_agent(
|
||||
host: str, port: int, plano_config_yaml: dict, additional_settings: dict
|
||||
) -> None:
|
||||
env = os.environ.copy()
|
||||
env.update(
|
||||
{
|
||||
|
|
@ -186,7 +207,6 @@ def start_cli_agent(plano_config_file=None, settings_json="{}"):
|
|||
"ANTHROPIC_SMALL_FAST_MODEL"
|
||||
]
|
||||
else:
|
||||
# Check if arch.claude.code.small.fast alias exists in model_aliases
|
||||
model_aliases = plano_config_yaml.get("model_aliases", {})
|
||||
if "arch.claude.code.small.fast" in model_aliases:
|
||||
env["ANTHROPIC_SMALL_FAST_MODEL"] = "arch.claude.code.small.fast"
|
||||
|
|
@ -196,23 +216,10 @@ def start_cli_agent(plano_config_file=None, settings_json="{}"):
|
|||
)
|
||||
log.info("Or provide ANTHROPIC_SMALL_FAST_MODEL in --settings JSON")
|
||||
|
||||
# Non-interactive mode configuration from additional_settings only
|
||||
if additional_settings.get("NON_INTERACTIVE_MODE", False):
|
||||
env.update(
|
||||
{
|
||||
"CI": "true",
|
||||
"FORCE_COLOR": "0",
|
||||
"NODE_NO_READLINE": "1",
|
||||
"TERM": "dumb",
|
||||
}
|
||||
)
|
||||
_apply_non_interactive_env(env, additional_settings)
|
||||
|
||||
# Build claude command arguments
|
||||
claude_args = []
|
||||
|
||||
# Add settings if provided, excluding those already handled as environment variables
|
||||
if additional_settings:
|
||||
# Filter out settings that are already processed as environment variables
|
||||
claude_settings = {
|
||||
k: v
|
||||
for k, v in additional_settings.items()
|
||||
|
|
@ -221,10 +228,8 @@ def start_cli_agent(plano_config_file=None, settings_json="{}"):
|
|||
if claude_settings:
|
||||
claude_args.append(f"--settings={json.dumps(claude_settings)}")
|
||||
|
||||
# Use claude from PATH
|
||||
claude_path = "claude"
|
||||
log.info(f"Connecting Claude Code Agent to Plano at {host}:{port}")
|
||||
|
||||
try:
|
||||
subprocess.run([claude_path] + claude_args, env=env, check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
|
|
@ -235,3 +240,61 @@ def start_cli_agent(plano_config_file=None, settings_json="{}"):
|
|||
f"{claude_path} not found. Make sure Claude Code is installed: npm install -g @anthropic-ai/claude-code"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _start_codex_cli_agent(host: str, port: int, additional_settings: dict) -> None:
|
||||
env = os.environ.copy()
|
||||
env.update(
|
||||
{
|
||||
"OPENAI_API_KEY": "test", # Use test token for plano
|
||||
"OPENAI_BASE_URL": f"http://{host}:{port}/v1",
|
||||
"NO_PROXY": host,
|
||||
"DISABLE_TELEMETRY": "true",
|
||||
}
|
||||
)
|
||||
_apply_non_interactive_env(env, additional_settings)
|
||||
|
||||
codex_model = additional_settings.get("CODEX_MODEL", "gpt-5.3-codex")
|
||||
codex_path = "codex"
|
||||
codex_args = ["--model", codex_model]
|
||||
|
||||
log.info(
|
||||
f"Connecting Codex CLI Agent to Plano at {host}:{port} (default model: {codex_model})"
|
||||
)
|
||||
try:
|
||||
subprocess.run([codex_path] + codex_args, env=env, check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
log.error(f"Error starting codex: {e}")
|
||||
sys.exit(1)
|
||||
except FileNotFoundError:
|
||||
log.error(
|
||||
f"{codex_path} not found. Make sure Codex CLI is installed: npm install -g @openai/codex"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def start_cli_agent(
|
||||
plano_config_file=None, cli_agent_type="claude", settings_json="{}"
|
||||
):
|
||||
"""Start a CLI client connected to Plano."""
|
||||
|
||||
with open(plano_config_file, "r") as file:
|
||||
plano_config = file.read()
|
||||
plano_config_yaml = yaml.safe_load(plano_config)
|
||||
|
||||
host, port = _resolve_cli_agent_endpoint(plano_config_yaml)
|
||||
|
||||
additional_settings = _parse_cli_agent_settings(settings_json)
|
||||
|
||||
if cli_agent_type == "claude":
|
||||
_start_claude_cli_agent(host, port, plano_config_yaml, additional_settings)
|
||||
return
|
||||
|
||||
if cli_agent_type == "codex":
|
||||
_start_codex_cli_agent(host, port, additional_settings)
|
||||
return
|
||||
|
||||
log.error(
|
||||
f"Unsupported cli agent type '{cli_agent_type}'. Supported values: claude, codex"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import json
|
||||
import os
|
||||
import multiprocessing
|
||||
import subprocess
|
||||
|
|
@ -31,6 +32,7 @@ from planoai.trace_cmd import trace as trace_cmd, start_trace_listener_backgroun
|
|||
from planoai.consts import (
|
||||
DEFAULT_OTEL_TRACING_GRPC_ENDPOINT,
|
||||
DEFAULT_NATIVE_OTEL_TRACING_GRPC_ENDPOINT,
|
||||
NATIVE_PID_FILE,
|
||||
PLANO_DOCKER_IMAGE,
|
||||
PLANO_DOCKER_NAME,
|
||||
)
|
||||
|
|
@ -40,6 +42,30 @@ from planoai.versioning import check_version_status, get_latest_version, get_ver
|
|||
log = getLogger(__name__)
|
||||
|
||||
|
||||
def _is_native_plano_running() -> bool:
|
||||
if not os.path.exists(NATIVE_PID_FILE):
|
||||
return False
|
||||
try:
|
||||
with open(NATIVE_PID_FILE, "r") as f:
|
||||
pids = json.load(f)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return False
|
||||
|
||||
envoy_pid = pids.get("envoy_pid")
|
||||
brightstaff_pid = pids.get("brightstaff_pid")
|
||||
if not isinstance(envoy_pid, int) or not isinstance(brightstaff_pid, int):
|
||||
return False
|
||||
|
||||
for pid in (envoy_pid, brightstaff_pid):
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
except ProcessLookupError:
|
||||
return False
|
||||
except PermissionError:
|
||||
continue
|
||||
return True
|
||||
|
||||
|
||||
def _is_port_in_use(port: int) -> bool:
|
||||
"""Check if a TCP port is already bound on localhost."""
|
||||
import socket
|
||||
|
|
@ -523,7 +549,7 @@ def logs(debug, follow, docker):
|
|||
|
||||
|
||||
@click.command()
|
||||
@click.argument("type", type=click.Choice(["claude"]), required=True)
|
||||
@click.argument("type", type=click.Choice(["claude", "codex"]), required=True)
|
||||
@click.argument("file", required=False) # Optional file argument
|
||||
@click.option(
|
||||
"--path", default=".", help="Path to the directory containing plano_config.yaml"
|
||||
|
|
@ -536,14 +562,19 @@ def logs(debug, follow, docker):
|
|||
def cli_agent(type, file, path, settings):
|
||||
"""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' or 'codex')
|
||||
"""
|
||||
|
||||
# Check if plano docker container is running
|
||||
plano_status = docker_container_status(PLANO_DOCKER_NAME)
|
||||
if plano_status != "running":
|
||||
log.error(f"plano docker container is not running (status: {plano_status})")
|
||||
log.error("Please start plano using the 'planoai up' command.")
|
||||
native_running = _is_native_plano_running()
|
||||
docker_running = False
|
||||
if not native_running:
|
||||
docker_running = docker_container_status(PLANO_DOCKER_NAME) == "running"
|
||||
|
||||
if not (native_running or docker_running):
|
||||
log.error("Plano is not running.")
|
||||
log.error(
|
||||
"Start Plano first using 'planoai up <config.yaml>' (native or --docker mode)."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# Determine plano_config.yaml path
|
||||
|
|
@ -553,7 +584,7 @@ def cli_agent(type, file, path, settings):
|
|||
sys.exit(1)
|
||||
|
||||
try:
|
||||
start_cli_agent(plano_config_file, settings)
|
||||
start_cli_agent(plano_config_file, type, settings)
|
||||
except SystemExit:
|
||||
# Re-raise SystemExit to preserve exit codes
|
||||
raise
|
||||
|
|
|
|||
2
cli/uv.lock
generated
2
cli/uv.lock
generated
|
|
@ -337,7 +337,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "planoai"
|
||||
version = "0.4.7"
|
||||
version = "0.4.9"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue