@click.group(
help="OpenJarvis — modular AI assistant backend",
invoke_without_command=True,
)
@click.version_option(version=openjarvis.__version__, prog_name="jarvis")
@click.option("--verbose", is_flag=True, default=False, help="Enable debug logging")
@click.option("--quiet", is_flag=True, default=False, help="Suppress non-error output")
@click.pass_context
def cli(ctx: click.Context, verbose: bool, quiet: bool) -> None:
"""Top-level CLI group."""
from openjarvis.cli.log_config import setup_logging
ctx.ensure_object(dict)
ctx.obj["verbose"] = verbose
ctx.obj["quiet"] = quiet
setup_logging(verbose=verbose, quiet=quiet)
# Check for updates on interactive commands. The banner is noise in
# demo recordings of ``jarvis ask --research``, so skip it whenever
# the research flag is in argv (cheap argv sniff — Click hasn't
# parsed the subcommand's args yet at this point).
import sys
research_mode_active = "--research" in sys.argv
if not quiet and ctx.invoked_subcommand and not research_mode_active:
import threading
from openjarvis.cli._version_check import check_for_updates
# Run the PyPI version poll off the hot path: on a cache miss it does
# a blocking urlopen (up to 3s) that otherwise delays every command,
# notably `jarvis serve` startup (#263). It's best-effort and never
# raises, and the nudge prints to stderr, so a daemon thread is safe —
# for long-lived commands (serve) it finishes; for short commands that
# exit first, the check is simply skipped this run (same as a miss).
threading.Thread(
target=check_for_updates,
args=(ctx.invoked_subcommand,),
daemon=True,
).start()
# First-run guard — routes bare `jarvis` to chat or init.
if ctx.invoked_subcommand is None:
from openjarvis.cli._first_run import check_and_route
check_and_route(ctx)