@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",
)
)