Files
omnara-agent-monitor/omnara/cli_backup.py
2025-08-11 11:08:59 -07:00

591 lines
20 KiB
Python

"""Omnara Main Entry Point (DEPRECATED BACKUP)
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!! THIS IS A DEPRECATED BACKUP FILE - DO NOT USE !!!
!!! The active CLI is in omnara/cli.py !!!
!!! This file is kept for reference only !!!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
This is a backup of the old CLI implementation that used flags instead of subcommands.
The new implementation uses a cleaner subcommand structure:
- omnara (default) -> Claude chat
- omnara serve -> Webhook server
- omnara mcp -> MCP stdio server
Original description:
This is the main entry point for the omnara command that dispatches to either:
- MCP stdio server (default or with --stdio)
- Claude Code webhook server (with --claude-code-webhook)
"""
import argparse
import sys
import subprocess
import json
import os
from pathlib import Path
import webbrowser
import urllib.parse
from http.server import HTTPServer, BaseHTTPRequestHandler
import secrets
import requests
import time
import threading
def get_current_version():
"""Get the current installed version of omnara"""
try:
from omnara import __version__
return __version__
except Exception:
return "unknown"
def check_for_updates():
"""Check PyPI for a newer version of omnara"""
try:
response = requests.get("https://pypi.org/pypi/omnara/json", timeout=2)
latest_version = response.json()["info"]["version"]
current_version = get_current_version()
if latest_version != current_version and current_version != "unknown":
print(f"\n✨ Update available: {current_version}{latest_version}")
print(" Run: pip install --upgrade omnara\n")
except Exception:
pass
def get_credentials_path():
"""Get the path to the credentials file"""
config_dir = Path.home() / ".omnara"
return config_dir / "credentials.json"
def load_stored_api_key():
"""Load API key from credentials file if it exists"""
credentials_path = get_credentials_path()
if not credentials_path.exists():
return None
try:
with open(credentials_path, "r") as f:
data = json.load(f)
api_key = data.get("write_key")
if api_key and isinstance(api_key, str):
return api_key
else:
print("Warning: Invalid API key format in credentials file.")
return None
except json.JSONDecodeError:
print(
"Warning: Corrupted credentials file. Please re-authenticate with --reauth."
)
return None
except (KeyError, IOError) as e:
print(f"Warning: Error reading credentials file: {str(e)}")
return None
def save_api_key(api_key):
"""Save API key to credentials file"""
credentials_path = get_credentials_path()
# Create directory if it doesn't exist
credentials_path.parent.mkdir(mode=0o700, exist_ok=True)
# Save the API key
data = {"write_key": api_key}
with open(credentials_path, "w") as f:
json.dump(data, f, indent=2)
# Set file permissions to 600 (read/write for owner only)
os.chmod(credentials_path, 0o600)
class AuthHTTPServer(HTTPServer):
"""Custom HTTP server with attributes for authentication"""
api_key: str | None
state: str | None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.api_key = None
self.state = None
class AuthCallbackHandler(BaseHTTPRequestHandler):
"""HTTP handler for the OAuth callback"""
def log_message(self, format, *args):
# Suppress default logging
pass
def do_GET(self):
# Parse query parameters
if "?" in self.path:
query_string = self.path.split("?", 1)[1]
params = urllib.parse.parse_qs(query_string)
# Verify state parameter
server: AuthHTTPServer = self.server # type: ignore
if "state" in params and params["state"][0] == server.state:
if "api_key" in params:
api_key = params["api_key"][0]
# Store the API key in the server instance
server.api_key = api_key
print("\n✓ Authentication successful!")
# Send success response with nice styling
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(b"""
<html>
<head>
<title>Omnara CLI - Authentication Successful</title>
<style>
body {
margin: 0;
padding: 0;
min-height: 100vh;
background: linear-gradient(135deg, #1a1618 0%, #2a1f3d 100%);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
display: flex;
align-items: center;
justify-content: center;
color: #fef3c7;
}
.card {
background: rgba(26, 22, 24, 0.8);
border: 1px solid rgba(245, 158, 11, 0.2);
border-radius: 12px;
padding: 48px;
text-align: center;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3),
0 0 60px rgba(245, 158, 11, 0.1);
max-width: 400px;
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.icon {
width: 64px;
height: 64px;
margin: 0 auto 24px;
background: rgba(134, 239, 172, 0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
animation: scaleIn 0.5s ease-out 0.2s both;
}
@keyframes scaleIn {
from { transform: scale(0); }
to { transform: scale(1); }
}
.checkmark {
width: 32px;
height: 32px;
stroke: #86efac;
stroke-width: 3;
fill: none;
stroke-dasharray: 100;
stroke-dashoffset: 100;
animation: draw 0.5s ease-out 0.5s forwards;
}
@keyframes draw {
to { stroke-dashoffset: 0; }
}
h1 {
margin: 0 0 16px;
font-size: 24px;
font-weight: 600;
color: #86efac;
}
p {
margin: 0;
opacity: 0.8;
line-height: 1.5;
}
.close-hint {
margin-top: 24px;
font-size: 14px;
opacity: 0.6;
}
</style>
</head>
<body>
<div class="card">
<div class="icon">
<svg class="checkmark" viewBox="0 0 24 24">
<path d="M20 6L9 17l-5-5" />
</svg>
</div>
<h1>Authentication Successful!</h1>
<p>Your CLI has been authorized to access Omnara.</p>
<p class="close-hint">Redirecting to dashboard in a moment...</p>
</div>
<script>
setTimeout(() => {
window.location.href = 'https://omnara.com/dashboard';
}, 2000);
</script>
</body>
</html>
""")
return
else:
# Invalid or missing state parameter
self.send_response(403)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(b"""
<html>
<head><title>Omnara CLI - Authentication Failed</title></head>
<body style="font-family: sans-serif; text-align: center; padding: 50px;">
<h1>Authentication Failed</h1>
<p>Invalid authentication state. Please try again.</p>
</body>
</html>
""")
return
# Send error response
self.send_response(400)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(b"""
<html>
<head><title>Omnara CLI - Authentication Failed</title></head>
<body style="font-family: sans-serif; text-align: center; padding: 50px;">
<h1>Authentication Failed</h1>
<p>No API key was received. Please try again.</p>
</body>
</html>
""")
def authenticate_via_browser(auth_url="https://omnara.com"):
"""Authenticate via browser and return the API key"""
# Generate a secure random state parameter
state = secrets.token_urlsafe(32)
# Start local server to receive the callback
server = AuthHTTPServer(("localhost", 0), AuthCallbackHandler)
server.state = state
server.api_key = None
port = server.server_port
# Construct the auth URL
auth_base = auth_url.rstrip("/")
callback_url = f"http://localhost:{port}"
auth_url = f"{auth_base}/cli-auth?callback={urllib.parse.quote(callback_url)}&state={urllib.parse.quote(state)}"
print("\nOpening browser for authentication...")
print("If your browser doesn't open automatically, please click this link:")
print(f"\n {auth_url}\n")
print("Waiting for authentication...")
# Run server in a thread
server_thread = threading.Thread(target=server.serve_forever)
server_thread.daemon = True
server_thread.start()
# Open browser
try:
webbrowser.open(auth_url)
except Exception:
pass
# Wait for authentication (with timeout)
start_time = time.time()
while not server.api_key and (time.time() - start_time) < 300:
time.sleep(0.1)
# Shutdown server in a separate thread to avoid deadlock
def shutdown_server():
server.shutdown()
shutdown_thread = threading.Thread(target=shutdown_server)
shutdown_thread.start()
shutdown_thread.join(timeout=1) # Wait max 1 second for shutdown
server.server_close()
if server.api_key:
return server.api_key
else:
raise Exception("Authentication failed - no API key received")
def run_stdio_server(args):
"""Run the MCP stdio server with the provided arguments"""
cmd = [
sys.executable,
"-m",
"servers.mcp_server.stdio_server",
"--api-key",
args.api_key,
]
if args.base_url:
cmd.extend(["--base-url", args.base_url])
if (
hasattr(args, "claude_code_permission_tool")
and args.claude_code_permission_tool
):
cmd.append("--claude-code-permission-tool")
if hasattr(args, "git_diff") and args.git_diff:
cmd.append("--git-diff")
if hasattr(args, "agent_instance_id") and args.agent_instance_id:
cmd.extend(["--agent-instance-id", args.agent_instance_id])
subprocess.run(cmd)
def run_webhook_server(
cloudflare_tunnel=False, dangerously_skip_permissions=False, port=None
):
"""Run the Claude Code webhook FastAPI server"""
cmd = [
sys.executable,
"-m",
"webhooks.claude_code",
]
if dangerously_skip_permissions:
cmd.append("--dangerously-skip-permissions")
if cloudflare_tunnel:
cmd.append("--cloudflare-tunnel")
if port is not None:
cmd.extend(["--port", str(port)])
print("[INFO] Starting Claude Code webhook server...")
subprocess.run(cmd)
def run_claude_wrapper(api_key, base_url=None, claude_args=None):
"""Run the Claude wrapper V3 for Omnara integration"""
# Import and run directly instead of subprocess
from webhooks.claude_wrapper_v3 import main as claude_wrapper_main
# Prepare sys.argv for the claude wrapper
original_argv = sys.argv
new_argv = ["claude_wrapper_v3", "--api-key", api_key]
if base_url:
new_argv.extend(["--base-url", base_url])
# Add any additional Claude arguments
if claude_args:
new_argv.extend(claude_args)
try:
sys.argv = new_argv
claude_wrapper_main()
finally:
sys.argv = original_argv
def main():
"""Main entry point that dispatches based on command line arguments"""
parser = argparse.ArgumentParser(
description="Omnara - AI Agent Dashboard and Tools",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Run Claude wrapper (default)
omnara --api-key YOUR_API_KEY
# Run Claude wrapper with custom base URL
omnara --api-key YOUR_API_KEY --base-url http://localhost:8000
# Run MCP stdio server
omnara --stdio --api-key YOUR_API_KEY
# Run webhook server with Cloudflare tunnel (recommended)
omnara --webhook
# Run webhook server with custom port
omnara --webhook --port 8080
# Run Claude Code webhook server without tunnel
omnara --claude-code-webhook
# Run webhook server with Cloudflare tunnel (verbose)
omnara --claude-code-webhook --cloudflare-tunnel
# Run with custom API base URL
omnara --stdio --api-key YOUR_API_KEY --base-url http://localhost:8000
# Run with custom frontend URL for authentication
omnara --auth-url http://localhost:3000
# Run with git diff capture enabled
omnara --stdio --api-key YOUR_API_KEY --git-diff
""",
)
# Add mutually exclusive group for server modes
mode_group = parser.add_mutually_exclusive_group()
mode_group.add_argument(
"--stdio",
action="store_true",
help="Run the MCP stdio server",
)
mode_group.add_argument(
"--claude-code-webhook",
action="store_true",
help="Run the Claude Code webhook server",
)
mode_group.add_argument(
"--claude",
action="store_true",
help="Run the Claude wrapper V3 for Omnara integration (default if no mode specified)",
)
mode_group.add_argument(
"--webhook",
action="store_true",
help="Run the webhook server with Cloudflare tunnel (shorthand for --claude-code-webhook --cloudflare-tunnel)",
)
# Arguments for webhook mode
parser.add_argument(
"--cloudflare-tunnel",
action="store_true",
help="Run Cloudflare tunnel for the webhook server (webhook mode only)",
)
parser.add_argument(
"--dangerously-skip-permissions",
action="store_true",
help="Skip permission prompts in Claude Code (webhook mode only) - USE WITH CAUTION",
)
parser.add_argument(
"--port",
type=int,
help="Port to run the webhook server on (webhook mode only, default: 6662)",
)
# Arguments for stdio mode
parser.add_argument(
"--api-key", help="API key for authentication (uses stored key if not provided)"
)
parser.add_argument(
"--reauth",
action="store_true",
help="Force re-authentication even if API key exists",
)
parser.add_argument(
"--version", action="store_true", help="Show the current version of omnara"
)
parser.add_argument(
"--base-url",
default="https://agent-dashboard-mcp.onrender.com",
help="Base URL of the Omnara API server (stdio mode only)",
)
parser.add_argument(
"--auth-url",
default="https://omnara.com",
help="Base URL of the Omnara frontend for authentication (default: https://omnara.com)",
)
parser.add_argument(
"--claude-code-permission-tool",
action="store_true",
help="Enable Claude Code permission prompt tool for handling tool execution approvals (stdio mode only)",
)
parser.add_argument(
"--git-diff",
action="store_true",
help="Enable git diff capture for log_step and ask_question (stdio mode only)",
)
parser.add_argument(
"--agent-instance-id",
type=str,
help="Pre-existing agent instance ID to use for this session (stdio mode only)",
)
# Use parse_known_args to capture remaining args for Claude
args, unknown_args = parser.parse_known_args()
# Handle --version flag
if args.version:
print(f"omnara version {get_current_version()}")
sys.exit(0)
# Check for updates (only when running actual commands, not --version)
check_for_updates()
if args.cloudflare_tunnel and not (args.claude_code_webhook or args.webhook):
parser.error(
"--cloudflare-tunnel can only be used with --claude-code-webhook or --webhook"
)
if args.port is not None and not (args.claude_code_webhook or args.webhook):
parser.error("--port can only be used with --claude-code-webhook or --webhook")
# Handle re-authentication
if args.reauth:
try:
print("Re-authenticating...")
api_key = authenticate_via_browser(args.auth_url)
save_api_key(api_key)
args.api_key = api_key
print("Re-authentication successful! API key saved.")
except Exception as e:
parser.error(f"Re-authentication failed: {str(e)}")
else:
# Load API key from storage if not provided
api_key = args.api_key
if not api_key and (
args.stdio or not (args.claude_code_webhook or args.webhook)
):
api_key = load_stored_api_key()
# Update args with the loaded API key
if api_key and not args.api_key:
args.api_key = api_key
if args.claude_code_webhook or args.webhook:
# If --webhook is used, enable cloudflare_tunnel by default
cloudflare_tunnel = args.cloudflare_tunnel or args.webhook
run_webhook_server(
cloudflare_tunnel=cloudflare_tunnel,
dangerously_skip_permissions=args.dangerously_skip_permissions,
port=args.port,
)
elif args.stdio:
if not args.api_key:
try:
print("No API key found. Starting authentication...")
api_key = authenticate_via_browser(args.auth_url)
save_api_key(api_key)
args.api_key = api_key
print("Authentication successful! API key saved.")
except Exception as e:
parser.error(f"Authentication failed: {str(e)}")
run_stdio_server(args)
else:
# Default to Claude mode when no mode is specified
if not args.api_key:
try:
print("No API key found. Starting authentication...")
api_key = authenticate_via_browser(args.auth_url)
save_api_key(api_key)
args.api_key = api_key
print("Authentication successful! API key saved.")
except Exception as e:
parser.error(f"Authentication failed: {str(e)}")
run_claude_wrapper(args.api_key, args.base_url, unknown_args)
if __name__ == "__main__":
main()