Skip to content

init_cmd

init_cmd

jarvis init — detect hardware, generate config, write to disk.

Classes

Functions

init

init(force: bool, config: Optional[Path], full_config: bool = False, engine: Optional[str] = None, no_download: bool = False, skip_scan: bool = False, host: Optional[str] = None, enable_digest: bool = False, preset: Optional[str] = None) -> None

Detect hardware and generate ~/.openjarvis/config.toml.

Source code in src/openjarvis/cli/init_cmd.py
@click.command()
@click.option(
    "--force", is_flag=True, help="Overwrite existing config without prompting."
)
@click.option(
    "--config",
    type=click.Path(exists=True),
    help="Path to config file to use.",
)
@click.option(
    "--full",
    "full_config",
    is_flag=True,
    help="Generate full reference config with all sections",
)
@click.option(
    "--engine",
    type=click.Choice(_SUPPORTED_ENGINES, case_sensitive=False),
    default=None,
    help="Inference engine to use (skips interactive selection).",
)
@click.option(
    "--no-download", is_flag=True, default=False, help="Skip the model download prompt."
)
@click.option(
    "--no-scan",
    "skip_scan",
    is_flag=True,
    default=False,
    help="Skip the post-init security environment audit.",
)
@click.option(
    "--host",
    default=None,
    help="Remote engine host URL (e.g. http://192.168.1.50:11434).",
)
@click.option(
    "--digest",
    "enable_digest",
    is_flag=True,
    default=False,
    help="Include Morning Digest config section.",
)
@click.option(
    "--preset",
    type=click.Choice(
        [
            "morning-digest-mac",
            "morning-digest-linux",
            "morning-digest-minimal",
            "deep-research",
            "code-assistant",
            "scheduled-monitor",
            "chat-simple",
        ],
        case_sensitive=False,
    ),
    default=None,
    help="Use a pre-built starter config instead of generating one.",
)
def init(
    force: bool,
    config: Optional[Path],
    full_config: bool = False,
    engine: Optional[str] = None,
    no_download: bool = False,
    skip_scan: bool = False,
    host: Optional[str] = None,
    enable_digest: bool = False,
    preset: Optional[str] = None,
) -> None:
    """Detect hardware and generate ~/.openjarvis/config.toml."""
    console = Console()

    if DEFAULT_CONFIG_PATH.exists() and not force:
        console.print(
            f"[yellow]Config already exists at {DEFAULT_CONFIG_PATH}[/yellow]"
        )
        console.print("Use [bold]--force[/bold] to overwrite.")
        raise SystemExit(1)

    # Handle --preset: copy a starter config and return early
    if preset:
        examples_dir = (
            Path(__file__).resolve().parents[2] / "configs" / "openjarvis" / "examples"
        )
        # Also check installed package location
        if not examples_dir.exists():
            examples_dir = (
                Path(__file__).resolve().parents[3]
                / "configs"
                / "openjarvis"
                / "examples"
            )
        preset_path = examples_dir / f"{preset}.toml"
        if not preset_path.exists():
            console.print(f"[red]Preset '{preset}' not found.[/red]")
            console.print(f"  Looked in: {examples_dir}")
            raise SystemExit(1)
        DEFAULT_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
        DEFAULT_CONFIG_PATH.write_text(preset_path.read_text())
        console.print(
            f"[green]Preset '{preset}' installed to {DEFAULT_CONFIG_PATH}[/green]"
        )
        console.print(
            "\n  Edit the config to customize, then run "
            "[bold]jarvis doctor[/bold] to verify."
        )
        return

    console.print("[bold]Detecting hardware...[/bold]")
    hw = detect_hardware()

    console.print(f"  Platform : {hw.platform}")
    console.print(f"  CPU      : {hw.cpu_brand} ({hw.cpu_count} cores)")
    console.print(f"  RAM      : {hw.ram_gb} GB")
    if hw.gpu:
        mem_label = "unified memory" if hw.gpu.vendor == "apple" else "VRAM"
        gpu = hw.gpu
        console.print(
            f"  GPU      : {gpu.name} ({gpu.vram_gb} GB {mem_label}, x{gpu.count})"
        )
    else:
        console.print("  GPU      : none detected")

    # Resolve engine: explicit flag > interactive selection > auto-detect
    if engine is None and config is None:
        recommended = recommend_engine(hw)
        console.print()
        console.print("[bold]Detecting running inference engines...[/bold]")
        running = _detect_running_engines()
        if running:
            console.print(f"  Found running: [green]{', '.join(running)}[/green]")
        else:
            console.print("  No running engines detected.")

        # Build choices: show running engines first, then recommended, then rest
        seen: set[str] = set()
        choices: list[str] = []
        for r in running:
            if r not in seen:
                choices.append(r)
                seen.add(r)
        if recommended not in seen:
            choices.append(recommended)
            seen.add(recommended)
        for e in _SUPPORTED_ENGINES:
            if e not in seen:
                choices.append(e)
                seen.add(e)

        # Default: first running engine, or hardware recommendation
        default = running[0] if running else recommended

        labels = []
        for c in choices:
            parts = [c]
            if c in running:
                parts.append("running")
            if c == recommended:
                parts.append("recommended")
            labels.append(
                f"  {c}" + (f"  ({', '.join(parts[1:])})" if len(parts) > 1 else "")
            )

        console.print()
        console.print("[bold]Available engines:[/bold]")
        for label in labels:
            console.print(label)

        engine = click.prompt(
            "\nSelect inference engine",
            type=click.Choice(choices, case_sensitive=False),
            default=default,
        )

    # Probe remote host if specified
    if host:
        console.print("\n[bold]Checking remote host...[/bold]")
        try:
            resp = httpx.get(host.rstrip("/") + "/", timeout=2.0)
            if resp.status_code < 500:
                console.print(f"  [green]Reachable[/green] ({host})")
            else:
                console.print(
                    f"  [yellow]Warning:[/yellow] Host returned status "
                    f"{resp.status_code} — writing config anyway."
                )
        except Exception:
            console.print(
                f"  [yellow]Warning:[/yellow] Host unreachable ({host}) "
                f"— writing config anyway."
            )

    if config:
        toml_content = config.read_text()
    else:
        if full_config:
            toml_content = generate_default_toml(hw, engine=engine, host=host)
        else:
            toml_content = generate_minimal_toml(hw, engine=engine, host=host)

    DEFAULT_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
    if config:
        config.write_text(toml_content)
    else:
        DEFAULT_CONFIG_PATH.write_text(toml_content)

    console.print()
    console.print(
        Panel(
            escape(toml_content),
            title=str(DEFAULT_CONFIG_PATH),
            border_style="green",
        )
    )
    # Append Morning Digest section if requested
    if enable_digest:
        digest_section = """
# ─── Morning Digest ─────────────────────────────────────────
[digest]
enabled = true
schedule = "0 7 * * *"
timezone = "America/Los_Angeles"
persona = "jarvis"
honorific = "sir"
tts_backend = "cartesia"
voice_id = "c8f7835e-28a3-4f0c-80d7-c1302ac62aae"
voice_speed = 1.2
sections = ["health", "messages", "calendar", "world"]

[digest.health]
sources = ["oura"]

[digest.messages]
sources = ["gmail", "google_tasks", "imessage"]

[digest.calendar]
sources = ["gcalendar"]

[digest.world]
sources = ["hackernews", "news_rss"]
"""
        target = config if config else DEFAULT_CONFIG_PATH
        existing = target.read_text()
        target.write_text(existing + digest_section)
        toml_content = target.read_text()
        console.print(
            "[green]Morning Digest config added.[/green] "
            "Run [bold]jarvis connect gdrive[/bold] to connect "
            "Google services, then [bold]jarvis digest --fresh[/bold]."
        )

    console.print("[green]Config written successfully.[/green]")

    # Create default memory files (skip if they already exist)
    soul_path = DEFAULT_CONFIG_DIR / "SOUL.md"
    if not soul_path.exists():
        soul_path.write_text(
            "# Agent Persona\n\nYou are Jarvis, a helpful personal AI assistant.\n"
        )

    memory_path = DEFAULT_CONFIG_DIR / "MEMORY.md"
    if not memory_path.exists():
        memory_path.write_text("# Agent Memory\n\n")

    user_path = DEFAULT_CONFIG_DIR / "USER.md"
    if not user_path.exists():
        user_path.write_text("# User Profile\n\n")

    skills_dir = DEFAULT_CONFIG_DIR / "skills"
    skills_dir.mkdir(exist_ok=True)

    selected_engine = engine or recommend_engine(hw)
    model = recommend_model(hw, selected_engine)

    if not model:
        console.print(
            "\n  [yellow]! Not enough memory to run any local model.[/yellow]\n"
            "  Consider a cloud engine or a machine with more RAM."
        )
    else:
        spec = find_model_spec(model)
        size_gb = estimated_download_gb(spec.parameter_count_b) if spec else 0
        from openjarvis.core.config import _available_memory_gb

        avail = _available_memory_gb(hw)
        console.print(
            f"\n  [bold]Recommended model:[/bold] {model} (~{size_gb:.1f} GB)"
            f"  [dim](selected for {avail:.0f} GB available memory)[/dim]"
        )

        if not no_download and spec:
            prompt = f"  Download {model} (~{size_gb:.1f} GB) now?"
            if click.confirm(prompt, default=True):
                _do_download(selected_engine, model, spec, console)
            else:
                console.print(
                    f"\n  Skipped. Download later with:\n"
                    f"    [bold]jarvis model pull {model}[/bold]"
                )

    if not skip_scan:
        _quick_privacy_check(console)
    console.print()
    console.print(
        Panel(
            _next_steps_text(selected_engine, model),
            title="Getting Started",
            border_style="cyan",
        )
    )