File size: 14,011 Bytes
92c4ae6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
#!/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 <command>")
        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()