#!/usr/bin/env python3 """ Atom OS CLI - Command-line interface for Atom AI automation platform. OpenClaw Integration: Single-command installer for "vibe coder" entry. Full-featured Atom with optional host mount (governance-first). Usage: pip install atom-os atom-os --help """ import os import sys import click import logging from pathlib import Path # Add backend to path for imports sys.path.insert(0, str(Path(__file__).parent.parent)) logger = logging.getLogger(__name__) @click.group() @click.version_option(version="0.1.0") def main_cli(): """ Atom OS - AI-powered business automation platform. Governance-first architecture with multi-agent system. """ pass @main_cli.command() @click.option('--port', default=8000, help='Port for web server') @click.option('--host', default='0.0.0.0', help='Host to bind to') @click.option('--workers', default=1, help='Number of worker processes') @click.option('--host-mount', is_flag=True, help='Enable host filesystem mount (Docker)') @click.option('--dev', is_flag=True, help='Enable development mode (auto-reload)') def start(port: int, host: str, workers: int, host_mount: bool, dev: bool): """ Start Atom OS server. Starts FastAPI server with optional host filesystem mount. **Host Mount Warning:** This gives containers write access to host directories. Only enable after reviewing governance protections: - AUTONOMOUS maturity gate required for shell access - Command whitelist (ls, cat, grep, git, npm, etc.) - Blocked commands (rm, mv, chmod, kill, sudo, etc.) - 5-minute timeout enforcement - Full audit trail to ShellSession table **Examples:** atom-os # Start on localhost:8000 atom-os --port 3000 --dev # Development mode with reload atom-os --host-mount # With host mount (requires confirmation) """ # Import after CLI to avoid slow startup from main_api_app import app # Check edition from core.package_feature_service import get_package_feature_service service = get_package_feature_service() edition_display = "Personal Edition" if service.is_personal else "Enterprise Edition" click.echo(click.style(f"{edition_display}", fg="blue" if service.is_personal else "yellow")) if host_mount: _confirm_host_mount() os.environ["ATOM_HOST_MOUNT_ENABLED"] = "true" # Configure host mount directories current_user = os.getenv("USER", "unknown") os.environ.setdefault( "ATOM_HOST_MOUNT_DIRS", f"/tmp:/Users/{current_user}/projects:/Users/{current_user}/Desktop:/Users/{current_user}/Documents" ) click.echo(click.style("⚠️ Host filesystem mount ENABLED", fg="yellow", bold=True)) click.echo("Governance protections active:") click.echo(" ✓ AUTONOMOUS maturity gate") click.echo(" ✓ Command whitelist") click.echo(" ✓ Blocked commands") click.echo(" ✓ 5-minute timeout") click.echo(" ✓ Audit trail") click.echo("") # Start server import uvicorn click.echo(click.style("🚀 Starting Atom OS...", fg="green", bold=True)) click.echo(f" Host: {host}") click.echo(f" Port: {port}") click.echo(f" Workers: {workers}") click.echo(f" Dev mode: {dev}") click.echo(f" Host mount: {host_mount}") click.echo("") click.echo(f" Dashboard: http://{host}:{port}") click.echo(f" API docs: http://{host}:{port}/docs") click.echo("") if dev: # Development mode with auto-reload uvicorn.run( "main_api_app:app", host=host, port=port, reload=True, log_level="info" ) else: # Production mode uvicorn.run( "main_api_app:app", host=host, port=port, workers=workers, log_level="info", access_log=True ) @main_cli.command() @click.option('--port', '-p', default=8000, help='Port for web server') @click.option('--host', '-h', default='0.0.0.0', help='Host to bind to') @click.option('--workers', '-w', default=1, help='Number of worker processes') @click.option('--host-mount', is_flag=True, help='Enable host filesystem mount') @click.option('--dev', is_flag=True, help='Enable development mode') @click.option('--foreground', '-f', is_flag=True, help='Run in foreground (not daemon)') def daemon(port: int, host: str, workers: int, host_mount: bool, dev: bool, foreground: bool): """Start Atom OS as background daemon service. Starts Atom OS as detached background process with PID file tracking. Use with other agents (OpenClaw, Claude, custom) for agent-to-agent execution. **Examples:** atom-os daemon # Start as daemon atom-os daemon --port 3000 # Custom port atom-os daemon --foreground # Run in foreground (for debugging) atom-os daemon --host-mount # With host mount """ from cli.daemon import DaemonManager, LOG_FILE if foreground: # Run in foreground mode (not daemon) click.echo(click.style("⚡ Starting Atom OS in foreground mode...", fg="yellow")) start(port, host, workers, host_mount, dev) else: # Run as daemon try: pid = DaemonManager.start_daemon(port, host, workers, host_mount, dev) click.echo(click.style("✓ Atom OS started as daemon", fg="green", bold=True)) click.echo(f" PID: {pid}") click.echo(f" Dashboard: http://{host}:{port}") click.echo(f" Logs: {LOG_FILE}") click.echo("") click.echo("Control commands:") click.echo(" atom-os status - Check status") click.echo(" atom-os stop - Stop daemon") except RuntimeError as e: click.echo(click.style(f"Error: {e}", fg="red"), err=True) raise SystemExit(1) except IOError as e: click.echo(click.style(f"Error: {e}", fg="red"), err=True) raise SystemExit(1) @main_cli.command() def stop(): """Stop Atom OS background daemon. Gracefully shuts down daemon process with cleanup. **Example:** atom-os stop """ from cli.daemon import DaemonManager if DaemonManager.stop_daemon(): pid = DaemonManager.get_pid() if pid: click.echo(click.style(f"✓ Atom OS stopped (PID: {pid})", fg="green", bold=True)) else: click.echo(click.style("✓ Atom OS stopped", fg="green", bold=True)) else: click.echo(click.style("ℹ Atom OS was not running", fg="yellow")) @main_cli.command() def status(): """Check Atom OS daemon status. Shows running state, PID, uptime, memory usage, and CPU. **Example:** atom-os status """ from cli.daemon import DaemonManager status_info = DaemonManager.get_status() if not status_info["running"]: click.echo("Status: " + click.style("STOPPED", fg="red")) else: click.echo("Status: " + click.style("RUNNING", fg="green", bold=True)) click.echo(f" PID: {status_info['pid']}") if status_info.get('memory_mb'): click.echo(f" Memory: {status_info['memory_mb']:.1f} MB") if status_info.get('cpu_percent'): click.echo(f" CPU: {status_info['cpu_percent']:.1f}%") if status_info.get('uptime_seconds'): click.echo(f" Uptime: {status_info['uptime_seconds']:.0f}s") # Show dashboard URL if running port = os.getenv("PORT", "8000") click.echo(f" Dashboard: http://localhost:{port}") # Show note if present if status_info.get("note"): click.echo(f" Note: {status_info['note']}") @main_cli.command() @click.argument('command', required=False) def execute(command: str): """Execute Atom command on-demand and return result. Starts Atom temporarily, executes a command, and shuts down. Useful for one-off agent tasks. **Examples:** atom-os execute "agent.chat('Hello, create a report')" atom-os execute "workflow.run('monthly_report')" **Note:** Command routing not yet implemented - use REST API instead. """ if not command: click.echo("Error: command required") click.echo("Usage: atom-os execute ") raise SystemExit(1) click.echo(click.style("⚡ Executing Atom command...", fg="yellow")) click.echo(f"Command: {command}") click.echo("") click.echo("(Command routing not yet implemented - use REST API instead)") click.echo("") click.echo("For programmatic control, use:") click.echo(" POST /api/agent/start - Start Atom as service") click.echo(" POST /api/agent/execute - Execute single command") click.echo("") click.echo("See docs: atom-os config") @main_cli.command() @click.option('--show-daemon', is_flag=True, help='Show daemon status') def config(show_daemon: bool): """Show configuration details and environment variables.""" click.echo(click.style("Atom OS Configuration", bold=True)) click.echo("=" * 40) click.echo("") click.echo("Environment Variables:") click.echo("") click.echo("Server:") click.echo(" PORT - Server port (default: 8000)") click.echo(" HOST - Server host (default: 0.0.0.0)") click.echo(" WORKERS - Worker processes (default: 1)") click.echo("") click.echo("Daemon:") click.echo(" ATOM_DAEMON_MODE - Enable daemon mode (true/false)") click.echo("") click.echo("Host Mount (SECURITY WARNING):") click.echo(" ATOM_HOST_MOUNT_ENABLED - Enable host filesystem mount") click.echo(" ATOM_HOST_MOUNT_DIRS - Allowed directories (colon-separated)") click.echo("") click.echo("Database:") click.echo(" DATABASE_URL - Database connection string") click.echo("") click.echo("LLM Providers:") click.echo(" OPENAI_API_KEY - OpenAI API key") click.echo(" ANTHROPIC_API_KEY - Anthropic API key") click.echo(" DEEPSEEK_API_KEY - DeepSeek API key") click.echo("") click.echo("Agent-to-Agent Execution:") click.echo(" POST /api/agent/start - Start Atom as service") click.echo(" POST /api/agent/stop - Stop Atom service") click.echo(" GET /api/agent/status - Check status") click.echo(" POST /api/agent/execute - Execute command") click.echo("") click.echo("See .env file for full configuration.") if show_daemon: from cli.daemon import DaemonManager, PID_FILE, LOG_FILE click.echo("") click.echo("Daemon Configuration:") click.echo(f" PID File: {PID_FILE}") click.echo(f" Log File: {LOG_FILE}") click.echo(f" Running: {DaemonManager.is_running()}") @main_cli.command() def config(): """Show configuration details and environment variables.""" click.echo(click.style("Atom OS Configuration", bold=True)) click.echo("=" * 40) click.echo("") click.echo("Environment Variables:") click.echo("") click.echo("Server:") click.echo(" PORT - Server port (default: 8000)") click.echo(" HOST - Server host (default: 0.0.0.0)") click.echo(" WORKERS - Worker processes (default: 1)") click.echo("") click.echo("Host Mount (SECURITY WARNING):") click.echo(" ATOM_HOST_MOUNT_ENABLED - Enable host filesystem mount") click.echo(" ATOM_HOST_MOUNT_DIRS - Allowed directories (colon-separated)") click.echo("") click.echo("Database:") click.echo(" DATABASE_URL - Database connection string") click.echo("") click.echo("LLM Providers:") click.echo(" OPENAI_API_KEY - OpenAI API key") click.echo(" ANTHROPIC_API_KEY - Anthropic API key") click.echo(" DEEPSEEK_API_KEY - DeepSeek API key") click.echo("") click.echo("Local Agent:") click.echo(" ATOM_BACKEND_URL - Backend API URL (default: http://localhost:8000)") click.echo("") click.echo("See .env file for full configuration.") # Import local-agent command group from cli.local_agent import local_agent main_cli.add_command(local_agent, name="local-agent") # Import edition management commands from cli.init import register_init_command from cli.enable import register_enable_command # Register edition management commands register_init_command(main_cli) register_enable_command(main_cli) def _confirm_host_mount(): """Interactive confirmation for host mount.""" click.echo(click.style("⚠️ HOST FILESYSTEM MOUNT", fg="yellow", bold=True)) click.echo("") click.echo("You are about to enable host filesystem access for Atom containers.") click.echo("") click.echo("This gives containers WRITE access to host directories.") click.echo("") click.echo("Governance protections in place:") click.echo(" ✓ AUTONOMOUS maturity gate required for shell access") click.echo(" ✓ Command whitelist (ls, cat, grep, git, npm, etc.)") click.echo(" ✓ Blocked commands (rm, mv, chmod, kill, sudo, etc.)") click.echo(" ✓ 5-minute timeout enforcement") click.echo(" ✓ Full audit trail to ShellSession table") click.echo("") click.echo("However, this STILL carries risk:") click.echo(" - Bugs in governance code could bypass protections") click.echo(" - Compromised AUTONOMOUS agent has shell access") click.echo(" - Docker escape vulnerabilities could be exploited") click.echo("") if not click.confirm("Do you understand the risks and want to continue?"): click.echo("Host mount cancelled.") raise SystemExit(1) click.echo("") click.echo("✓ Host mount confirmed") if __name__ == "__main__": main_cli()