Merge branch 'main' of https://github.com/katanemo/plano into musa/codex-cli

This commit is contained in:
Musa 2026-03-05 14:24:16 -08:00
commit 102223d011
No known key found for this signature in database
13 changed files with 50 additions and 60 deletions

View file

@ -133,13 +133,13 @@ jobs:
load: true load: true
tags: | tags: |
${{ env.PLANO_DOCKER_IMAGE }} ${{ env.PLANO_DOCKER_IMAGE }}
${{ env.DOCKER_IMAGE }}:0.4.10 ${{ env.DOCKER_IMAGE }}:0.4.11
${{ env.DOCKER_IMAGE }}:latest ${{ env.DOCKER_IMAGE }}:latest
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
- name: Save image as artifact - name: Save image as artifact
run: docker save ${{ env.PLANO_DOCKER_IMAGE }} ${{ env.DOCKER_IMAGE }}:0.4.10 ${{ env.DOCKER_IMAGE }}:latest -o /tmp/plano-image.tar run: docker save ${{ env.PLANO_DOCKER_IMAGE }} ${{ env.DOCKER_IMAGE }}:0.4.11 ${{ env.DOCKER_IMAGE }}:latest -o /tmp/plano-image.tar
- name: Upload image artifact - name: Upload image artifact
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v6
@ -163,7 +163,7 @@ jobs:
python-version: "3.14" python-version: "3.14"
- name: Install planoai - name: Install planoai
run: pip install ./cli run: pip install -e ./cli
- name: Validate plano config - name: Validate plano config
run: bash config/validate_plano_config.sh run: bash config/validate_plano_config.sh

View file

@ -72,7 +72,7 @@ COPY cli/README.md ./
COPY config/plano_config_schema.yaml /config/plano_config_schema.yaml COPY config/plano_config_schema.yaml /config/plano_config_schema.yaml
COPY config/envoy.template.yaml /config/envoy.template.yaml COPY config/envoy.template.yaml /config/envoy.template.yaml
RUN uv run pip install --no-cache-dir . RUN pip install --no-cache-dir -e .
COPY cli/planoai planoai/ COPY cli/planoai planoai/
COPY config/envoy.template.yaml . COPY config/envoy.template.yaml .

View file

@ -24,7 +24,7 @@ export function Hero() {
> >
<div className="inline-flex flex-wrap items-center gap-1.5 sm:gap-2 px-3 sm:px-4 py-1 rounded-full bg-[rgba(185,191,255,0.4)] border border-[var(--secondary)] shadow backdrop-blur hover:bg-[rgba(185,191,255,0.6)] transition-colors cursor-pointer"> <div className="inline-flex flex-wrap items-center gap-1.5 sm:gap-2 px-3 sm:px-4 py-1 rounded-full bg-[rgba(185,191,255,0.4)] border border-[var(--secondary)] shadow backdrop-blur hover:bg-[rgba(185,191,255,0.6)] transition-colors cursor-pointer">
<span className="text-xs sm:text-sm font-medium text-black/65"> <span className="text-xs sm:text-sm font-medium text-black/65">
v0.4.10 v0.4.11
</span> </span>
<span className="text-xs sm:text-sm font-medium text-black "> <span className="text-xs sm:text-sm font-medium text-black ">

View file

@ -1 +1 @@
docker build -f Dockerfile . -t katanemo/plano -t katanemo/plano:0.4.10 docker build -f Dockerfile . -t katanemo/plano -t katanemo/plano:0.4.11

View file

@ -1,3 +1,3 @@
"""Plano CLI - Intelligent Prompt Gateway.""" """Plano CLI - Intelligent Prompt Gateway."""
__version__ = "0.4.10" __version__ = "0.4.11"

View file

@ -5,7 +5,7 @@ PLANO_COLOR = "#969FF4"
SERVICE_NAME_ARCHGW = "plano" SERVICE_NAME_ARCHGW = "plano"
PLANO_DOCKER_NAME = "plano" PLANO_DOCKER_NAME = "plano"
PLANO_DOCKER_IMAGE = os.getenv("PLANO_DOCKER_IMAGE", "katanemo/plano:0.4.10") PLANO_DOCKER_IMAGE = os.getenv("PLANO_DOCKER_IMAGE", "katanemo/plano:0.4.11")
DEFAULT_OTEL_TRACING_GRPC_ENDPOINT = "http://localhost:4317" DEFAULT_OTEL_TRACING_GRPC_ENDPOINT = "http://localhost:4317"
# Native mode constants # Native mode constants

View file

@ -179,7 +179,7 @@ def build(docker):
cwd=crates_dir, cwd=crates_dir,
check=True, check=True,
) )
console.print("[green]✓[/green] WASM plugins built") log.info("WASM plugins built")
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
console.print(f"[red]✗[/red] WASM build failed: {e}") console.print(f"[red]✗[/red] WASM build failed: {e}")
sys.exit(1) sys.exit(1)
@ -197,7 +197,7 @@ def build(docker):
cwd=crates_dir, cwd=crates_dir,
check=True, check=True,
) )
console.print("[green]✓[/green] brightstaff built") log.info("brightstaff built")
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
console.print(f"[red]✗[/red] brightstaff build failed: {e}") console.print(f"[red]✗[/red] brightstaff build failed: {e}")
sys.exit(1) sys.exit(1)
@ -319,7 +319,7 @@ def up(file, path, foreground, with_tracing, tracing_port, docker):
console.print(f" [dim]{validation_stderr.strip()}[/dim]") console.print(f" [dim]{validation_stderr.strip()}[/dim]")
sys.exit(1) sys.exit(1)
console.print(f"[green]✓[/green] Configuration valid") log.info("Configuration valid")
# Set up environment # Set up environment
default_otel = ( default_otel = (

View file

@ -100,15 +100,13 @@ def ensure_envoy_binary():
with open(version_path, "r") as f: with open(version_path, "r") as f:
cached_version = f.read().strip() cached_version = f.read().strip()
if cached_version == ENVOY_VERSION: if cached_version == ENVOY_VERSION:
log.info(f"Envoy {ENVOY_VERSION} found at {envoy_path}") log.info(f"Envoy {ENVOY_VERSION} (cached)")
return envoy_path return envoy_path
print( log.info(
f"Envoy version changed ({cached_version}{ENVOY_VERSION}), re-downloading..." f"Envoy version changed ({cached_version}{ENVOY_VERSION}), re-downloading..."
) )
else: else:
log.info( log.info("Envoy binary found (unknown version, re-downloading...)")
f"Envoy binary found at {envoy_path} (unknown version, re-downloading...)"
)
slug = _get_platform_slug() slug = _get_platform_slug()
url = ( url = (
@ -123,6 +121,7 @@ def ensure_envoy_binary():
try: try:
_download_file(url, tmp_path, label=f"Envoy {ENVOY_VERSION}") _download_file(url, tmp_path, label=f"Envoy {ENVOY_VERSION}")
log.info(f"Extracting Envoy {ENVOY_VERSION}...")
with tarfile.open(tmp_path, "r:xz") as tar: with tarfile.open(tmp_path, "r:xz") as tar:
# Find the envoy binary inside the archive # Find the envoy binary inside the archive
envoy_member = None envoy_member = None
@ -150,7 +149,6 @@ def ensure_envoy_binary():
os.chmod(envoy_path, 0o755) os.chmod(envoy_path, 0o755)
with open(version_path, "w") as f: with open(version_path, "w") as f:
f.write(ENVOY_VERSION) f.write(ENVOY_VERSION)
log.info(f"Envoy {ENVOY_VERSION} installed at {envoy_path}")
return envoy_path return envoy_path
finally: finally:
@ -187,7 +185,7 @@ def ensure_wasm_plugins():
# 1. Local source build (inside repo) # 1. Local source build (inside repo)
local = _find_local_wasm_plugins() local = _find_local_wasm_plugins()
if local: if local:
log.info(f"Using locally-built WASM plugins: {local[0]}") log.info("Using locally-built WASM plugins")
return local return local
# 2. Cached download # 2. Cached download
@ -201,9 +199,9 @@ def ensure_wasm_plugins():
with open(version_path, "r") as f: with open(version_path, "r") as f:
cached_version = f.read().strip() cached_version = f.read().strip()
if cached_version == version: if cached_version == version:
log.info(f"WASM plugins {version} found at {PLANO_PLUGINS_DIR}") log.info(f"WASM plugins {version} (cached)")
return prompt_gw_path, llm_gw_path return prompt_gw_path, llm_gw_path
print( log.info(
f"WASM plugins version changed ({cached_version}{version}), re-downloading..." f"WASM plugins version changed ({cached_version}{version}), re-downloading..."
) )
else: else:
@ -220,6 +218,7 @@ def ensure_wasm_plugins():
url = f"{PLANO_RELEASE_BASE_URL}/{version}/{gz_name}" url = f"{PLANO_RELEASE_BASE_URL}/{version}/{gz_name}"
gz_dest = dest + ".gz" gz_dest = dest + ".gz"
_download_file(url, gz_dest, label=f"{name} ({version})") _download_file(url, gz_dest, label=f"{name} ({version})")
log.info(f"Decompressing {name}...")
with gzip.open(gz_dest, "rb") as f_in, open(dest, "wb") as f_out: with gzip.open(gz_dest, "rb") as f_in, open(dest, "wb") as f_out:
shutil.copyfileobj(f_in, f_out) shutil.copyfileobj(f_in, f_out)
os.unlink(gz_dest) os.unlink(gz_dest)
@ -235,7 +234,7 @@ def ensure_brightstaff_binary():
# 1. Local source build (inside repo) # 1. Local source build (inside repo)
local = _find_local_brightstaff() local = _find_local_brightstaff()
if local: if local:
log.info(f"Using locally-built brightstaff: {local}") log.info("Using locally-built brightstaff")
return local return local
# 2. Cached download # 2. Cached download
@ -248,9 +247,9 @@ def ensure_brightstaff_binary():
with open(version_path, "r") as f: with open(version_path, "r") as f:
cached_version = f.read().strip() cached_version = f.read().strip()
if cached_version == version: if cached_version == version:
log.info(f"brightstaff {version} found at {brightstaff_path}") log.info(f"brightstaff {version} (cached)")
return brightstaff_path return brightstaff_path
print( log.info(
f"brightstaff version changed ({cached_version}{version}), re-downloading..." f"brightstaff version changed ({cached_version}{version}), re-downloading..."
) )
else: else:
@ -265,6 +264,7 @@ def ensure_brightstaff_binary():
gz_path = brightstaff_path + ".gz" gz_path = brightstaff_path + ".gz"
_download_file(url, gz_path, label=f"brightstaff ({version}, {slug})") _download_file(url, gz_path, label=f"brightstaff ({version}, {slug})")
log.info("Decompressing brightstaff...")
with gzip.open(gz_path, "rb") as f_in, open(brightstaff_path, "wb") as f_out: with gzip.open(gz_path, "rb") as f_in, open(brightstaff_path, "wb") as f_out:
shutil.copyfileobj(f_in, f_out) shutil.copyfileobj(f_in, f_out)
os.unlink(gz_path) os.unlink(gz_path)
@ -272,7 +272,6 @@ def ensure_brightstaff_binary():
os.chmod(brightstaff_path, 0o755) os.chmod(brightstaff_path, 0o755)
with open(version_path, "w") as f: with open(version_path, "w") as f:
f.write(version) f.write(version)
log.info(f"brightstaff {version} installed at {brightstaff_path}")
return brightstaff_path return brightstaff_path

View file

@ -161,19 +161,10 @@ def start_native(plano_config_file, env, foreground=False, with_tracing=False):
"""Start Envoy and brightstaff natively.""" """Start Envoy and brightstaff natively."""
from planoai.core import _get_gateway_ports from planoai.core import _get_gateway_ports
console = None # Stop any existing instance first
try: if os.path.exists(NATIVE_PID_FILE):
from rich.console import Console log.info("Stopping existing Plano instance...")
stop_native()
console = Console()
except ImportError:
pass
def status_print(msg):
if console:
console.print(msg)
else:
print(msg)
envoy_path = ensure_envoy_binary() envoy_path = ensure_envoy_binary()
ensure_wasm_plugins() ensure_wasm_plugins()
@ -182,7 +173,7 @@ def start_native(plano_config_file, env, foreground=False, with_tracing=False):
plano_config_file, env, with_tracing=with_tracing plano_config_file, env, with_tracing=with_tracing
) )
status_print(f"[green]✓[/green] Configuration rendered") log.info("Configuration rendered")
log_dir = os.path.join(PLANO_RUN_DIR, "logs") log_dir = os.path.join(PLANO_RUN_DIR, "logs")
os.makedirs(log_dir, exist_ok=True) os.makedirs(log_dir, exist_ok=True)
@ -233,7 +224,7 @@ def start_native(plano_config_file, env, foreground=False, with_tracing=False):
# Health check # Health check
gateway_ports = _get_gateway_ports(plano_config_file) gateway_ports = _get_gateway_ports(plano_config_file)
status_print(f"[dim]Waiting for listeners to become healthy...[/dim]") log.info("Waiting for listeners to become healthy...")
start_time = time.time() start_time = time.time()
timeout = 60 timeout = 60
@ -244,35 +235,35 @@ def start_native(plano_config_file, env, foreground=False, with_tracing=False):
all_healthy = False all_healthy = False
if all_healthy: if all_healthy:
status_print(f"[green]✓[/green] Plano is running (native mode)") log.info("Plano is running (native mode)")
for port in gateway_ports: for port in gateway_ports:
status_print(f" [cyan]http://localhost:{port}[/cyan]") log.info(f" http://localhost:{port}")
break break
# Check if processes are still alive # Check if processes are still alive
if not _is_pid_alive(brightstaff_pid): if not _is_pid_alive(brightstaff_pid):
status_print("[red]✗[/red] brightstaff exited unexpectedly") log.error("brightstaff exited unexpectedly")
status_print(f" Check logs: {os.path.join(log_dir, 'brightstaff.log')}") log.error(f" Check logs: {os.path.join(log_dir, 'brightstaff.log')}")
_kill_pid(envoy_pid) _kill_pid(envoy_pid)
sys.exit(1) sys.exit(1)
if not _is_pid_alive(envoy_pid): if not _is_pid_alive(envoy_pid):
status_print("[red]✗[/red] envoy exited unexpectedly") log.error("envoy exited unexpectedly")
status_print(f" Check logs: {os.path.join(log_dir, 'envoy.log')}") log.error(f" Check logs: {os.path.join(log_dir, 'envoy.log')}")
_kill_pid(brightstaff_pid) _kill_pid(brightstaff_pid)
sys.exit(1) sys.exit(1)
if time.time() - start_time > timeout: if time.time() - start_time > timeout:
status_print(f"[red]✗[/red] Health check timed out after {timeout}s") log.error(f"Health check timed out after {timeout}s")
status_print(f" Check logs in: {log_dir}") log.error(f" Check logs in: {log_dir}")
stop_native() stop_native()
sys.exit(1) sys.exit(1)
time.sleep(1) time.sleep(1)
if foreground: if foreground:
status_print(f"[dim]Running in foreground. Press Ctrl+C to stop.[/dim]") log.info("Running in foreground. Press Ctrl+C to stop.")
status_print(f"[dim]Logs: {log_dir}[/dim]") log.info(f"Logs: {log_dir}")
try: try:
import glob import glob
@ -290,13 +281,13 @@ def start_native(plano_config_file, env, foreground=False, with_tracing=False):
) )
tail_proc.wait() tail_proc.wait()
except KeyboardInterrupt: except KeyboardInterrupt:
status_print(f"\n[dim]Stopping Plano...[/dim]") log.info("Stopping Plano...")
if tail_proc.poll() is None: if tail_proc.poll() is None:
tail_proc.terminate() tail_proc.terminate()
stop_native() stop_native()
else: else:
status_print(f"[dim]Logs: {log_dir}[/dim]") log.info(f"Logs: {log_dir}")
status_print(f"[dim]Run 'planoai down' to stop.[/dim]") log.info("Run 'planoai down' to stop.")
def _daemon_exec(args, env, log_path): def _daemon_exec(args, env, log_path):
@ -364,7 +355,7 @@ def _kill_pid(pid):
def stop_native(): def stop_native():
"""Stop natively-running Envoy and brightstaff processes.""" """Stop natively-running Envoy and brightstaff processes."""
if not os.path.exists(NATIVE_PID_FILE): if not os.path.exists(NATIVE_PID_FILE):
print("No native Plano instance found (PID file missing).") log.info("No native Plano instance found (PID file missing).")
return return
with open(NATIVE_PID_FILE, "r") as f: with open(NATIVE_PID_FILE, "r") as f:
@ -383,7 +374,7 @@ def stop_native():
log.info(f"{name} (PID {pid}) already stopped") log.info(f"{name} (PID {pid}) already stopped")
continue continue
except PermissionError: except PermissionError:
log.info(f"Permission denied stopping {name} (PID {pid})") log.error(f"Permission denied stopping {name} (PID {pid})")
continue continue
# Wait for graceful shutdown # Wait for graceful shutdown
@ -403,7 +394,7 @@ def stop_native():
pass pass
os.unlink(NATIVE_PID_FILE) os.unlink(NATIVE_PID_FILE)
print("Plano stopped (native mode).") log.info("Plano stopped (native mode).")
def native_validate_config(plano_config_file): def native_validate_config(plano_config_file):

View file

@ -1,6 +1,6 @@
[project] [project]
name = "planoai" name = "planoai"
version = "0.4.10" version = "0.4.11"
description = "Python-based CLI tool to manage Plano." description = "Python-based CLI tool to manage Plano."
authors = [{name = "Katanemo Labs, Inc."}] authors = [{name = "Katanemo Labs, Inc."}]
readme = "README.md" readme = "README.md"

View file

@ -17,7 +17,7 @@ from sphinxawesome_theme.postprocess import Icons
project = "Plano Docs" project = "Plano Docs"
copyright = "2025, Katanemo Labs, Inc" copyright = "2025, Katanemo Labs, Inc"
author = "Katanemo Labs, Inc" author = "Katanemo Labs, Inc"
release = " v0.4.10" release = " v0.4.11"
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

View file

@ -43,7 +43,7 @@ Plano's CLI allows you to manage and interact with the Plano efficiently. To ins
.. code-block:: console .. code-block:: console
$ uv tool install planoai==0.4.10 $ uv tool install planoai==0.4.11
**Option 2: Install with pip (Traditional)** **Option 2: Install with pip (Traditional)**
@ -51,7 +51,7 @@ Plano's CLI allows you to manage and interact with the Plano efficiently. To ins
$ python -m venv venv $ python -m venv venv
$ source venv/bin/activate # On Windows, use: venv\Scripts\activate $ source venv/bin/activate # On Windows, use: venv\Scripts\activate
$ pip install planoai==0.4.10 $ pip install planoai==0.4.11
.. _llm_routing_quickstart: .. _llm_routing_quickstart:

View file

@ -65,7 +65,7 @@ Create a ``docker-compose.yml`` file with the following configuration:
# docker-compose.yml # docker-compose.yml
services: services:
plano: plano:
image: katanemo/plano:0.4.10 image: katanemo/plano:0.4.11
container_name: plano container_name: plano
ports: ports:
- "10000:10000" # ingress (client -> plano) - "10000:10000" # ingress (client -> plano)