From 71c5b2bce12ca317dd2fea993356afaa038e5fb0 Mon Sep 17 00:00:00 2001 From: Robby Manihani Date: Sat, 23 May 2026 07:57:32 -0700 Subject: [PATCH] feat(cli): arc-reactor startup banner for init/serve/ask (#397) --- src/openjarvis/cli/_banner.py | 41 ++++++++++++++++++++++++++++++++++ src/openjarvis/cli/ask.py | 5 +++++ src/openjarvis/cli/init_cmd.py | 4 ++++ src/openjarvis/cli/serve.py | 4 ++++ 4 files changed, 54 insertions(+) create mode 100644 src/openjarvis/cli/_banner.py diff --git a/src/openjarvis/cli/_banner.py b/src/openjarvis/cli/_banner.py new file mode 100644 index 00000000..f3952caa --- /dev/null +++ b/src/openjarvis/cli/_banner.py @@ -0,0 +1,41 @@ +"""Startup banner — arc-reactor logo + OpenJarvis wordmark.""" + +# ruff: noqa: E501 — Rich markup tags inflate source-line length; the rendered +# banner stays under 80 displayed columns. + +from __future__ import annotations + +# Reactor on the left (5 lines, ~12 cols) + small ASCII wordmark on the right. +# Last line carries the tagline so the whole block stays under 8 lines / 80 cols. +_BANNER_LINES = ( + r" [blue]╭─────╮[/blue] [bold bright_blue] ___ _ _ [/]", + r" [blue]╱[/blue] [bright_blue]╭───╮[/bright_blue] [blue]╲[/blue] [bold bright_blue]/ _ \ _ __ ___ _ _ | | __ _ _ ___ _(_)___[/]", + r" [blue]│[/blue] [bright_blue]│[/bright_blue] [bold bright_white]◉[/] [bright_blue]│[/bright_blue] [blue]│[/blue] [bold bright_blue]| | | | '_ \/ -_) ' \ | |/ _` | '_\ V /| (_-<[/]", + r" [blue]╲[/blue] [bright_blue]╰───╯[/bright_blue] [blue]╱[/blue] [bold bright_blue]\___/| .__/\___|_||_||_|\__,_|_| \_/ |_/__/[/]", + r" [blue]╰─────╯[/blue] [dim]|_|[/dim] [cyan]Private AI on your machine[/cyan]", +) + +_PLAIN_BANNER = ( + r" ╭─────╮ ___ _ _ ", + r" ╱ ╭───╮ ╲ / _ \ _ __ ___ _ _ | | __ _ _ ___ _(_)___ ", + r" │ │ ◉ │ │ | | | | '_ \/ -_) ' \ | |/ _` | '_\ V /| (_-< ", + r" ╲ ╰───╯ ╱ \___/| .__/\___|_||_||_|\__,_|_| \_/ |_/__/ ", + r" ╰─────╯ |_| Private AI on your machine ", +) + + +def print_banner(quiet: bool = False) -> None: + """Print the OpenJarvis startup banner. No-op when quiet.""" + if quiet: + return + try: + from rich.console import Console + + console = Console() + for line in _BANNER_LINES: + console.print(line, highlight=False) + console.print() + except ImportError: + for line in _PLAIN_BANNER: + print(line) + print() diff --git a/src/openjarvis/cli/ask.py b/src/openjarvis/cli/ask.py index 06782b8a..fa61d39c 100644 --- a/src/openjarvis/cli/ask.py +++ b/src/openjarvis/cli/ask.py @@ -11,6 +11,7 @@ import click from rich.console import Console from rich.table import Table +from openjarvis.cli._banner import print_banner from openjarvis.cli._tool_names import resolve_tool_names from openjarvis.cli.hints import hint_no_engine from openjarvis.core.config import load_config @@ -587,7 +588,9 @@ def _print_profile( "(default: ~/.openjarvis/knowledge.db)." ), ) +@click.pass_context def ask( + ctx: click.Context, query: tuple[str, ...], model_name: str | None, engine_key: str | None, @@ -603,6 +606,8 @@ def ask( knowledge_db: str | None, ) -> None: """Ask Jarvis a question.""" + quiet = (ctx.obj or {}).get("quiet", False) or output_json + print_banner(quiet=quiet) console = Console(stderr=True) query_text = " ".join(query) diff --git a/src/openjarvis/cli/init_cmd.py b/src/openjarvis/cli/init_cmd.py index e69baf09..869c2746 100644 --- a/src/openjarvis/cli/init_cmd.py +++ b/src/openjarvis/cli/init_cmd.py @@ -11,6 +11,7 @@ from rich.console import Console from rich.markup import escape from rich.panel import Panel +from openjarvis.cli._banner import print_banner from openjarvis.cli._bootstrap import detect_cloud_keys from openjarvis.cli.model import find_model_spec, hf_download, ollama_pull from openjarvis.cli.scan_cmd import PrivacyScanner @@ -290,7 +291,9 @@ def _do_download(engine: str, model: str, spec, console: Console) -> None: hidden=True, help="Run init non-interactively; called by the bare-jarvis first-run guard.", ) +@click.pass_context def init( + ctx: click.Context, force: bool, config: Optional[Path], full_config: bool = False, @@ -303,6 +306,7 @@ def init( from_bare_jarvis: bool = False, ) -> None: """Detect hardware and generate ~/.openjarvis/config.toml.""" + print_banner(quiet=(ctx.obj or {}).get("quiet", False)) console = Console() # Cloud auto-detect — inform user if a key is in env. diff --git a/src/openjarvis/cli/serve.py b/src/openjarvis/cli/serve.py index 3bda0864..ef69c4ec 100644 --- a/src/openjarvis/cli/serve.py +++ b/src/openjarvis/cli/serve.py @@ -8,6 +8,7 @@ import sys import click from rich.console import Console +from openjarvis.cli._banner import print_banner from openjarvis.core.config import load_config from openjarvis.core.events import EventBus from openjarvis.engine import ( @@ -40,7 +41,9 @@ logger = logging.getLogger(__name__) default=None, help="Agent for non-streaming requests (simple, orchestrator, react, openhands).", ) +@click.pass_context def serve( + ctx: click.Context, host: str | None, port: int | None, engine_key: str | None, @@ -48,6 +51,7 @@ def serve( agent_name: str | None, ) -> None: """Start the OpenAI-compatible API server.""" + print_banner(quiet=(ctx.obj or {}).get("quiet", False)) console = Console(stderr=True) # Check for server dependencies