#!/usr/bin/env python3 """ Aetheel CLI =========== Click-based command-line interface for Aetheel. Usage: aetheel Start with default adapters aetheel start --discord Start with Discord adapter aetheel chat "Hello" One-shot AI query aetheel status Show runtime status aetheel doctor Run diagnostics aetheel config show Show current config aetheel cron list List scheduled jobs aetheel memory search "q" Search memory """ import json import os import subprocess import sys import click from config import ( CONFIG_PATH, AetheelConfig, load_config, save_default_config, write_mcp_config, ) @click.group(invoke_without_command=True) @click.pass_context def cli(ctx): """Aetheel — AI-Powered Personal Assistant""" if ctx.invoked_subcommand is None: ctx.invoke(start) @cli.command() @click.option("--discord", is_flag=True, help="Override: enable Discord adapter") @click.option("--telegram", is_flag=True, help="Override: enable Telegram adapter") @click.option("--webchat", is_flag=True, help="Override: enable WebChat adapter") @click.option("--claude", is_flag=True, help="Override: use Claude Code runtime") @click.option("--model", default=None, help="Override: model to use") @click.option("--test", is_flag=True, help="Use echo handler for testing") @click.option("--log", default="INFO", help="Log level") def start(discord, telegram, webchat, claude, model, test, log): """Start Aetheel (all settings from config, flags are optional overrides).""" # Build sys.argv equivalent and delegate to main.main() argv = ["main.py"] if discord: argv.append("--discord") if telegram: argv.append("--telegram") if webchat: argv.append("--webchat") if claude: argv.append("--claude") if model: argv.extend(["--model", model]) if test: argv.append("--test") if log and log != "INFO": argv.extend(["--log", log]) # Patch sys.argv so argparse inside main.main() sees our flags old_argv = sys.argv sys.argv = argv try: from main import main main() finally: sys.argv = old_argv @cli.command() @click.argument("message") def chat(message): """One-shot chat with the AI.""" import logging from dotenv import load_dotenv load_dotenv() save_default_config() cfg = load_config() logging.basicConfig( level=logging.WARNING, format="%(asctime)s [%(name)s] %(levelname)s: %(message)s", ) # Write MCP config if needed write_mcp_config(cfg.mcp, cfg.memory.workspace, False) # Initialize runtime from agent.opencode_runtime import ( OpenCodeConfig, OpenCodeRuntime, RuntimeMode, build_aetheel_system_prompt, ) oc_config = OpenCodeConfig( mode=RuntimeMode.SDK if cfg.runtime.mode == "sdk" else RuntimeMode.CLI, server_url=cfg.runtime.server_url, timeout_seconds=cfg.runtime.timeout_seconds, model=cfg.runtime.model, provider=cfg.runtime.provider, workspace_dir=cfg.runtime.workspace, format=cfg.runtime.format, ) runtime = OpenCodeRuntime(oc_config) # Run one-shot query system_prompt = build_aetheel_system_prompt() response = runtime.chat(message, system_prompt=system_prompt) click.echo(response.text if hasattr(response, "text") else str(response)) @cli.command() def status(): """Show runtime status.""" from dotenv import load_dotenv load_dotenv() save_default_config() cfg = load_config() click.echo("🟢 Aetheel Status") click.echo() click.echo(f" Config: {CONFIG_PATH}") click.echo(f" Runtime: {cfg.runtime.mode}, model={cfg.runtime.model or 'default'}") click.echo(f" Workspace: {cfg.memory.workspace}") click.echo(f" Heartbeat: {'enabled' if cfg.heartbeat.enabled else 'disabled'}") click.echo(f" WebChat: {'enabled' if cfg.webchat.enabled else 'disabled'}") # Show scheduler jobs if possible try: from scheduler import Scheduler sched = Scheduler(callback=lambda j: None) sched.start() jobs = sched.list_jobs() click.echo(f" Jobs: {len(jobs)}") sched.stop() except Exception: click.echo(" Jobs: (scheduler unavailable)") # --------------------------------------------------------------------------- # cron group # --------------------------------------------------------------------------- @cli.group() def cron(): """Manage scheduled jobs.""" pass @cron.command("list") def cron_list(): """List scheduled jobs.""" from dotenv import load_dotenv load_dotenv() save_default_config() try: from scheduler import Scheduler sched = Scheduler(callback=lambda j: None) sched.start() jobs = sched.list_jobs() if not jobs: click.echo("No scheduled jobs.") else: for job in jobs: click.echo( f" [{job.job_id}] {job.job_type} — {job.prompt[:60]}" ) sched.stop() except Exception as e: click.echo(f"Error listing jobs: {e}") @cron.command("remove") @click.argument("job_id") def cron_remove(job_id): """Remove a scheduled job.""" from dotenv import load_dotenv load_dotenv() save_default_config() try: from scheduler import Scheduler sched = Scheduler(callback=lambda j: None) sched.start() sched.remove_job(job_id) click.echo(f"Removed job {job_id}") sched.stop() except Exception as e: click.echo(f"Error removing job: {e}") # --------------------------------------------------------------------------- # config group # --------------------------------------------------------------------------- @cli.group() def config(): """Manage configuration.""" pass @config.command("show") def config_show(): """Show current configuration.""" if os.path.isfile(CONFIG_PATH): with open(CONFIG_PATH, "r", encoding="utf-8") as f: click.echo(f.read()) else: click.echo(f"No config file found at {CONFIG_PATH}") click.echo("Run 'aetheel config init' to create one.") @config.command("edit") def config_edit(): """Open config in editor.""" save_default_config() editor = os.environ.get("EDITOR", "nano") try: subprocess.run([editor, CONFIG_PATH], check=True) except FileNotFoundError: click.echo(f"Editor '{editor}' not found. Set $EDITOR or edit manually:") click.echo(f" {CONFIG_PATH}") except subprocess.CalledProcessError as e: click.echo(f"Editor exited with error: {e}") @config.command("init") def config_init(): """Reset config to defaults.""" # Force-write by removing existing file first if os.path.isfile(CONFIG_PATH): os.remove(CONFIG_PATH) click.echo(f"Removed existing config at {CONFIG_PATH}") path = save_default_config() click.echo(f"Default config written to {path}") # --------------------------------------------------------------------------- # memory group # --------------------------------------------------------------------------- @cli.group() def memory(): """Manage memory system.""" pass @memory.command("search") @click.argument("query") def memory_search(query): """Search memory.""" import asyncio import logging from dotenv import load_dotenv load_dotenv() save_default_config() cfg = load_config() logging.basicConfig(level=logging.WARNING) try: from memory import MemoryManager from memory.types import MemoryConfig as MemConfig workspace_dir = os.path.expanduser(cfg.memory.workspace) db_path = os.path.expanduser(cfg.memory.db_path) mem_config = MemConfig(workspace_dir=workspace_dir, db_path=db_path) mem = MemoryManager(mem_config) results = asyncio.run(mem.search(query)) if not results: click.echo("No results found.") else: for r in results: score = getattr(r, "score", "?") text = getattr(r, "text", str(r)) click.echo(f" [{score}] {text[:120]}") mem.close() except Exception as e: click.echo(f"Memory search error: {e}") @memory.command("sync") def memory_sync(): """Force memory re-index.""" import asyncio import logging from dotenv import load_dotenv load_dotenv() save_default_config() cfg = load_config() logging.basicConfig(level=logging.WARNING) try: from memory import MemoryManager from memory.types import MemoryConfig as MemConfig workspace_dir = os.path.expanduser(cfg.memory.workspace) db_path = os.path.expanduser(cfg.memory.db_path) mem_config = MemConfig(workspace_dir=workspace_dir, db_path=db_path) mem = MemoryManager(mem_config) stats = asyncio.run(mem.sync()) click.echo( f"Sync complete: {stats.get('files_indexed', 0)} files, " f"{stats.get('chunks_created', 0)} chunks" ) mem.close() except Exception as e: click.echo(f"Memory sync error: {e}") # --------------------------------------------------------------------------- # doctor command # --------------------------------------------------------------------------- @cli.command() def doctor(): """Run diagnostics.""" import shutil from dotenv import load_dotenv load_dotenv() click.echo("🩺 Aetheel Doctor") click.echo() # Check config if os.path.isfile(CONFIG_PATH): click.echo(f" ✅ Config file: {CONFIG_PATH}") try: with open(CONFIG_PATH, "r") as f: json.load(f) click.echo(" ✅ Config JSON is valid") except json.JSONDecodeError as e: click.echo(f" ❌ Config JSON invalid: {e}") else: click.echo(f" ⚠️ No config file at {CONFIG_PATH}") # Check workspace cfg = load_config() workspace = os.path.expanduser(cfg.memory.workspace) if os.path.isdir(workspace): click.echo(f" ✅ Workspace: {workspace}") else: click.echo(f" ⚠️ Workspace missing: {workspace}") # Check runtimes if shutil.which("claude"): click.echo(" ✅ Claude Code CLI found") else: click.echo(" ⚠️ Claude Code CLI not found") if shutil.which("opencode"): click.echo(" ✅ OpenCode CLI found") else: click.echo(" ⚠️ OpenCode CLI not found") # Check tokens tokens = { "SLACK_BOT_TOKEN": os.environ.get("SLACK_BOT_TOKEN"), "SLACK_APP_TOKEN": os.environ.get("SLACK_APP_TOKEN"), "TELEGRAM_BOT_TOKEN": os.environ.get("TELEGRAM_BOT_TOKEN"), "DISCORD_BOT_TOKEN": os.environ.get("DISCORD_BOT_TOKEN"), } click.echo() click.echo(" Tokens:") for name, val in tokens.items(): if val: click.echo(f" ✅ {name} is set") else: click.echo(f" ⚠️ {name} not set") # Check memory DB db_path = os.path.expanduser(cfg.memory.db_path) if os.path.isfile(db_path): click.echo(f" ✅ Memory DB: {db_path}") else: click.echo(f" ⚠️ Memory DB missing: {db_path}") click.echo() click.echo("Done.") # --------------------------------------------------------------------------- # Entry point # --------------------------------------------------------------------------- if __name__ == "__main__": cli()