"""
Brynq Runtime CLI — The terminal launcher for Brynq.

This is how users launch and interact with Brynq from their terminal.
Clean, fast, minimal — like ``claude`` for Claude Code.

Usage:
    brynq-runtime start              Start runtime + open browser
    brynq-runtime login claude       OAuth login for Claude
    brynq-runtime login gemini       OAuth login for Gemini
    brynq-runtime models             List available models
    brynq-runtime chat               Interactive terminal chat
    brynq-runtime chain "prompt"     Run a multi-model chain
    brynq-runtime status             Show runtime status
    brynq-runtime stop               Stop runtime

Environment:
    BRYNQ_RUNTIME_LOCAL_PORT    Runtime server port (default: 8003)
    BRYNQ_RUNTIME_OLLAMA_URL    Ollama URL (default: http://localhost:11434)
    BRYNQ_RUNTIME_DATA_DIR      Data directory (default: ~/.brynq)
"""

from __future__ import annotations

import argparse
import io
import json
import os
import shutil
import signal
import subprocess
import sys
import time
import urllib.error
import urllib.request
import webbrowser
from pathlib import Path
from typing import Any

from runtime.config import RuntimeConfig, load_config

# ── Constants ─────────────────────────────────────────────────────────

from runtime.version import get_version

VERSION = get_version()
APP_URL = "http://localhost:3000"
_PID_FILENAME = "runtime.pid"

# ANSI colors (disabled on Windows without VT100 support)
_NO_COLOR = os.environ.get("NO_COLOR", "")


def _supports_color() -> bool:
    """Detect whether the terminal supports ANSI color."""
    if _NO_COLOR:
        return False
    if sys.platform == "win32":
        return os.environ.get("WT_SESSION") is not None or "ANSICON" in os.environ
    return hasattr(sys.stdout, "isatty") and sys.stdout.isatty()


_COLOR = _supports_color()


def _dim(text: str) -> str:
    return f"\033[2m{text}\033[0m" if _COLOR else text


def _bold(text: str) -> str:
    return f"\033[1m{text}\033[0m" if _COLOR else text


def _green(text: str) -> str:
    return f"\033[32m{text}\033[0m" if _COLOR else text


def _red(text: str) -> str:
    return f"\033[31m{text}\033[0m" if _COLOR else text


def _yellow(text: str) -> str:
    return f"\033[33m{text}\033[0m" if _COLOR else text


def _cyan(text: str) -> str:
    return f"\033[36m{text}\033[0m" if _COLOR else text


# ── Helpers ───────────────────────────────────────────────────────────


def _pid_file(config: RuntimeConfig) -> Path:
    return config.data_dir / _PID_FILENAME


def _is_runtime_running(config: RuntimeConfig) -> bool:
    """Check if the runtime server is responding on its port."""
    try:
        url = f"http://127.0.0.1:{config.local_port}/status"
        req = urllib.request.Request(url, method="GET")
        with urllib.request.urlopen(req, timeout=3.0) as resp:
            data = json.loads(resp.read().decode("utf-8"))
            return data.get("status") == "ok"
    except Exception:
        return False


def _runtime_get(config: RuntimeConfig, path: str, timeout: float = 10.0) -> dict[str, Any]:
    """GET request to the running runtime server."""
    url = f"http://127.0.0.1:{config.local_port}{path}"
    req = urllib.request.Request(url, method="GET")
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        return json.loads(resp.read().decode("utf-8"))


def _runtime_post(
    config: RuntimeConfig, path: str, body: dict[str, Any], timeout: float = 120.0,
) -> dict[str, Any]:
    """POST request to the running runtime server."""
    url = f"http://127.0.0.1:{config.local_port}{path}"
    data = json.dumps(body).encode("utf-8")
    req = urllib.request.Request(
        url, data=data, headers={"Content-Type": "application/json"}, method="POST",
    )
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        return json.loads(resp.read().decode("utf-8"))


def _ollama_is_running(ollama_url: str) -> bool:
    """Check if Ollama is reachable."""
    try:
        req = urllib.request.Request(f"{ollama_url}/api/tags", method="GET")
        with urllib.request.urlopen(req, timeout=3.0):
            return True
    except Exception:
        return False


def _ollama_list_models(ollama_url: str) -> list[str]:
    """Get local Ollama model names."""
    try:
        req = urllib.request.Request(f"{ollama_url}/api/tags", method="GET")
        with urllib.request.urlopen(req, timeout=5.0) as resp:
            data = json.loads(resp.read().decode("utf-8"))
            return [m["name"] for m in data.get("models", [])]
    except Exception:
        return []


def _read_keys(data_dir: Path) -> dict[str, str]:
    """Read stored API keys from disk."""
    path = data_dir / "keys.json"
    if not path.exists():
        return {}
    try:
        return json.loads(path.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        return {}


def _read_tokens(data_dir: Path) -> dict[str, dict[str, Any]]:
    """Read stored OAuth tokens from disk."""
    path = data_dir / "oauth_tokens.json"
    if not path.exists():
        return {}
    try:
        return json.loads(path.read_text("utf-8"))
    except (json.JSONDecodeError, OSError):
        return {}


def _write_tokens(data_dir: Path, tokens: dict[str, dict[str, Any]]) -> None:
    """Write OAuth tokens to disk."""
    data_dir.mkdir(parents=True, exist_ok=True)
    (data_dir / "oauth_tokens.json").write_text(json.dumps(tokens, indent=2), "utf-8")


_CATALOG_CACHE: Any = None
_CATALOG_CACHE_TIME = 0.0

def _get_catalog_and_stats() -> tuple[Any, dict[str, Any]]:
    """Get the marketplace catalog and stats, caching for 60 seconds to avoid disk IO."""
    global _CATALOG_CACHE, _CATALOG_CACHE_TIME
    now = time.time()
    if _CATALOG_CACHE is not None and (now - _CATALOG_CACHE_TIME) < 60:
        return _CATALOG_CACHE

    try:
        from cloud.marketplace.catalog import MarketplaceCatalog
        for data_path in [
            Path(__file__).resolve().parent.parent / "cloud" / "data",
            Path.cwd() / "cloud" / "data",
            Path("cloud/data"),
        ]:
            catalog_file = data_path / "marketplace" / "catalog.json"
            if catalog_file.exists():
                cat = MarketplaceCatalog(data_path)
                stats = cat.stats()
                _CATALOG_CACHE = (cat, stats)
                _CATALOG_CACHE_TIME = now
                return _CATALOG_CACHE
    except Exception:
        pass

    _CATALOG_CACHE = (None, {"total_entries": 0, "by_category": {}})
    _CATALOG_CACHE_TIME = now
    return _CATALOG_CACHE


# ── Commands ──────────────────────────────────────────────────────────


def _get_installed_agents(data_dir: Path) -> list[dict]:
    agents_dir = data_dir / "agents"
    if not agents_dir.exists():
        return []
    installed = []
    for mf in agents_dir.glob("*/manifest.json"):
        try:
            installed.append(json.loads(mf.read_text(encoding="utf-8")))
        except Exception:
            pass
    return installed

def _require_learning(func_name: str) -> bool:
    """Check if learning modules are available locally."""
    try:
        import runtime.learning.usage_tracker  # noqa
        return True
    except ImportError:
        print(f"  Analytics not available in this installation.")
        print(f"  Install from source for full analytics, or check brynq.ai")
        return False

def cmd_stats(args: argparse.Namespace, config: RuntimeConfig) -> int:
    if not _require_learning("stats"): return 1
    from runtime.learning.usage_tracker import UsageTracker
    tracker = UsageTracker(config.data_dir)
    stats = tracker.get_stats()
    print("--- Usage Stats (Last 30 Days) ---")
    for k, v in stats.items():
        print(f"{k}: {v}")
    return 0

def cmd_insights(args: argparse.Namespace, config: RuntimeConfig) -> int:
    if not _require_learning("insights"): return 1
    from runtime.learning.usage_tracker import UsageTracker
    from runtime.learning.patterns import PatternDetector
    tracker = UsageTracker(config.data_dir)
    detector = PatternDetector(tracker)
    report = detector.generate_report()
    import json
    print("--- Insights Report ---")
    print(json.dumps(report, indent=2))
    return 0

def cmd_learn(args: argparse.Namespace, config: RuntimeConfig) -> int:
    if not _require_learning("learn"): return 1
    from runtime.learning.usage_tracker import UsageTracker
    from runtime.learning.preferences import Preferences
    tracker = UsageTracker(config.data_dir)
    prefs = Preferences(config.data_dir)
    learned = prefs.auto_detect(tracker)
    print("--- Auto-Detected Preferences ---")
    for k, v in learned.items():
        print(f"{k}: {v}")
    return 0

def cmd_preferences(args: argparse.Namespace, config: RuntimeConfig) -> int:
    if not _require_learning("preferences"): return 1
    from runtime.learning.preferences import Preferences
    prefs = Preferences(config.data_dir)
    if getattr(args, "set", None):
        if "=" in args.set:
            k, v = args.set.split("=", 1)
            prefs.save(k, v)
            print(f"Saved {k}={v}")
        else:
            print("Usage: --set key=value")
            return 1
    else:
        all_p = prefs.get_all()
        print("--- Preferences ---")
        for k, v in all_p.items():
            print(f"{k}: {v}")
    return 0

def cmd_reset_learning(args: argparse.Namespace, config: RuntimeConfig) -> int:
    print("Are you sure you want to clear all learning data? (y/n)")
    ans = input().strip().lower()
    if ans == 'y':
        import shutil
        ldir = config.data_dir / "learning"
        if ldir.exists():
            shutil.rmtree(ldir)
        print("Learning data cleared.")
    return 0

def cmd_trends(args: argparse.Namespace, config: RuntimeConfig) -> int:
    if not _require_learning("trends"): return 1
    from runtime.learning.usage_tracker import UsageTracker
    tracker = UsageTracker(config.data_dir)
    trend = tracker.daily_trend()
    print("--- 30 Day Trend ---")
    for d in trend:
        print(f"{d['date']}: {'*' * min(d['count'], 50)} ({d['count']})")
    return 0

def cmd_costs(args: argparse.Namespace, config: RuntimeConfig) -> int:
    if not _require_learning("costs"): return 1
    from runtime.learning.usage_tracker import UsageTracker
    from runtime.learning.cost_tracker import CostTracker
    tracker = UsageTracker(config.data_dir)
    costs = CostTracker(tracker)
    
    if getattr(args, "budget", None):
        costs.set_budget(args.budget)
        print(f"Daily budget set to ${args.budget}")
        return 0
        
    rep = costs.weekly_report()
    print("--- Weekly Cost Report ---")
    print(f"Total USD: ${rep['total_usd']:.4f}")
    print(f"Top Model: {rep['top_cost_model']}")
    print(f"Avg Daily: ${rep['avg_daily_cost']:.4f}")
    
    budget_status = costs.check_budget()
    print("\n--- Budget Status ---")
    print(f"Budget: ${budget_status['budget']:.4f}")
    print(f"Spent Today: ${budget_status['spent_today']:.4f}")
    print(f"Remaining: ${budget_status['remaining']:.4f}")
    return 0

def cmd_ab(args: argparse.Namespace, config: RuntimeConfig) -> int:
    if not _require_learning("ab"): return 1
    from runtime.learning.ab_testing import ABTestManager
    mgr = ABTestManager(config.data_dir)
    
    if getattr(args, "create", None):
        mgr.create_test(args.create, {"strategy": "sequential"}, {"strategy": "debate"})
        print(f"Created A/B test: {args.create}")
    elif getattr(args, "conclude", None):
        winner = mgr.conclude_test(args.conclude)
        print(f"Concluded {args.conclude}. Winner config: {winner}")
    elif getattr(args, "delete", None):
        mgr.delete_test(args.delete)
        print(f"Deleted test: {args.delete}")
    else:
        tests = mgr.list_tests()
        print("--- A/B Tests ---")
        for t in tests:
            print(f"{t['name']} - Status: {t['status']}")
    return 0

def cmd_network(args: argparse.Namespace, config: RuntimeConfig) -> int:
    if not _require_learning("network"): return 1
    from runtime.network.telemetry import TelemetryManager
    from runtime.network.insights import NetworkInsights
    
    tm = TelemetryManager(config.data_dir)
    if not tm.is_opted_in():
        print("Telemetry is not opted in. Run 'brynq-runtime preferences --set telemetry_enabled=True' to opt in and see network insights.")
        return 1
        
    insights = NetworkInsights(config.data_dir, config.cloud_url)
    print(insights.display_insights())
    return 0

def cmd_leaderboard(args: argparse.Namespace, config: RuntimeConfig) -> int:
    if not _require_learning("leaderboard"): return 1
    from runtime.network.insights import NetworkInsights
    insights = NetworkInsights(config.data_dir, config.cloud_url)
    lb = insights.fetch_leaderboard()
    
    if not lb:
        print("Leaderboard is empty or unreachable.")
        return 1
        
    print(f"{'Rank':<5} | {'Model':<30} | {'Score':<6} | {'Popularity'}")
    print("-" * 65)
    for row in lb:
        print(f"{row['rank']:<5} | {row['model']:<30} | {row['score']:<6.2f} | {row['popularity']}")
    return 0

def cmd_rate_agent(args: argparse.Namespace, config: RuntimeConfig) -> int:
    import urllib.request
    import json
    
    if args.rating < 1 or args.rating > 5:
        print("Rating must be between 1 and 5.")
        return 1
        
    from runtime.network.telemetry import TelemetryManager
    tm = TelemetryManager(config.data_dir)
    instance_id = tm.get_instance_id()
    
    payload = {"rating": args.rating, "review": args.review, "instance_id": instance_id}
    try:
        req = urllib.request.Request(
            f"{config.cloud_url}/v1/agents/{args.agent_id}/rate",
            data=json.dumps(payload).encode("utf-8"),
            headers={"Content-Type": "application/json"},
            method="POST"
        )
        with urllib.request.urlopen(req, timeout=5) as resp:
            print("Rating submitted successfully.")
            return 0
    except Exception as e:
        print(f"Failed to submit rating: {e}")
        return 1

def cmd_agent_reviews(args: argparse.Namespace, config: RuntimeConfig) -> int:
    import urllib.request
    import json
    try:
        req = urllib.request.Request(f"{config.cloud_url}/v1/agents/{args.agent_id}/reviews")
        with urllib.request.urlopen(req, timeout=5) as resp:
            data = json.loads(resp.read().decode("utf-8"))
            
        print(f"--- Reviews for {args.agent_id} (Avg: {data.get('avg_rating', 0):.1f}) ---")
        for r in data.get("reviews", []):
            print(f"[{r['rating']} stars] - {r['review']}")
        return 0
    except Exception as e:
        print(f"Failed to fetch reviews: {e}")
        return 1

def cmd_start(args: argparse.Namespace, config: RuntimeConfig) -> int:
    """Start the runtime server and open the app in the browser."""
    # Check if already running
    if _is_runtime_running(config):
        print(f"Brynq is already running at http://127.0.0.1:{config.local_port}")
        print(f"App: {APP_URL}")
        if not args.no_browser:
            webbrowser.open(APP_URL)
        return 0

    # Check Ollama
    ollama_ok = _ollama_is_running(config.ollama_url)
    if ollama_ok:
        models = _ollama_list_models(config.ollama_url)
        if models:
            print(f"  Ollama: {_green('running')} ({', '.join(models)})")
        else:
            print(f"  Ollama: {_green('running')} (no models pulled)")
    else:
        print(f"  Ollama: {_dim('not running')} (local models unavailable)")

    # Check API keys
    keys = _read_keys(config.data_dir)
    if keys:
        providers = ", ".join(sorted(keys.keys()))
        print(f"  API keys: {_green(providers)}")
    else:
        print(f"  API keys: {_dim('none configured')}")

    # Start the server as a background process
    env = {**os.environ}
    env["BRYNQ_RUNTIME_LOCAL_PORT"] = str(config.local_port)
    if config.debug:
        env["BRYNQ_RUNTIME_DEBUG"] = "1"

    cmd = [
        sys.executable, "-m", "uvicorn",
        "runtime.app:app",
        "--host", "127.0.0.1",
        "--port", str(config.local_port),
    ]

    # Redirect output to log file
    log_dir = config.data_dir / "logs"
    log_dir.mkdir(parents=True, exist_ok=True)
    log_file = log_dir / "runtime.log"

    with open(log_file, "a") as lf:
        proc = subprocess.Popen(
            cmd,
            env=env,
            stdout=lf,
            stderr=lf,
            start_new_session=True,
        )

    # Save PID for later shutdown
    pid_file = _pid_file(config)
    config.data_dir.mkdir(parents=True, exist_ok=True)
    pid_file.write_text(str(proc.pid), "utf-8")

    # Wait for server to come up
    print()
    print("Starting Brynq runtime...", end="", flush=True)
    started = False
    for _ in range(30):
        time.sleep(0.5)
        if _is_runtime_running(config):
            started = True
            break
        print(".", end="", flush=True)

    if not started:
        print(f" {_red('failed')}")
        print(f"Server did not start. Check logs: {log_file}")
        return 1

    print(f" {_green('ready')}")
    print()
    print(f"  {_bold('Brynq is running')} at http://127.0.0.1:{config.local_port}")
    print(f"  App: {_cyan(APP_URL)}")
    print(f"  Logs: {_dim(str(log_file))}")
    print()
    print(f"  {_dim('Stop with:')} brynq-runtime stop")

    # Start AutoHealer
    try:
        from runtime.health.auto_healer import AutoHealer
        import threading
        healer = AutoHealer(config)
        t = threading.Thread(target=healer.start_monitoring, daemon=True)
        t.start()
        print(f"  {_dim('AutoHealer: monitoring system health')}")
    except Exception as e:
        print(f"  {_dim(f'AutoHealer failed to start: {e}')}")

    # Open browser
    if not args.no_browser:
        webbrowser.open(APP_URL)

    return 0


def cmd_stop(args: argparse.Namespace, config: RuntimeConfig) -> int:
    """Stop the runtime server."""
    pid_file = _pid_file(config)

    if not _is_runtime_running(config) and not pid_file.exists():
        print("Brynq is not running.")
        return 0

    # Try to kill by PID
    if pid_file.exists():
        try:
            pid = int(pid_file.read_text("utf-8").strip())
            os.kill(pid, signal.SIGTERM)
            print(f"Stopping Brynq (PID {pid})...", end="", flush=True)

            # Wait for shutdown
            for _ in range(20):
                time.sleep(0.25)
                try:
                    os.kill(pid, 0)  # Check if still alive
                except OSError:
                    break

            pid_file.unlink(missing_ok=True)
            print(f" {_green('stopped')}")
            return 0

        except (ValueError, OSError) as e:
            # PID file stale or process already dead
            pid_file.unlink(missing_ok=True)

    # Fallback: check if still responding
    if _is_runtime_running(config):
        print(f"{_yellow('Warning')}: Could not find server process. It may still be running on port {config.local_port}.")
        return 1

    print("Brynq stopped.")
    pid_file.unlink(missing_ok=True)
    return 0


def cmd_status(args: argparse.Namespace, config: RuntimeConfig) -> int:
    """Show full runtime status."""
    print(_bold("Brynq Runtime Status"))
    print()

    # Runtime server
    running = _is_runtime_running(config)
    if running:
        print(f"  Runtime:  {_green('running')} on port {config.local_port}")
    else:
        print(f"  Runtime:  {_red('stopped')}")

    # Ollama
    ollama_ok = _ollama_is_running(config.ollama_url)
    if ollama_ok:
        models = _ollama_list_models(config.ollama_url)
        if models:
            print(f"  Ollama:   {_green('running')} ({len(models)} models: {', '.join(models)})")
        else:
            print(f"  Ollama:   {_green('running')} (no models pulled)")
    else:
        print(f"  Ollama:   {_red('stopped')}")

    # Cloud connection (only if runtime is up)
    if running:
        try:
            status_data = _runtime_get(config, "/status")
            cloud = status_data.get("cloud", {})
            if cloud.get("connected"):
                print(f"  Cloud:    {_green('connected')} ({config.cloud_url})")
            else:
                print(f"  Cloud:    {_yellow('unreachable')}")
        except Exception:
            print(f"  Cloud:    {_dim('unknown')}")
    else:
        print(f"  Cloud:    {_dim('unknown (runtime stopped)')}")

    # API keys
    print()
    keys = _read_keys(config.data_dir)
    provider_labels = {
        "anthropic": "Claude",
        "openai": "ChatGPT",
        "google": "Gemini",
        "mistral": "Mistral",
        "cohere": "Cohere",
        "groq": "Groq",
    }

    print("  API Keys:")
    if keys:
        for provider, _key in sorted(keys.items()):
            label = provider_labels.get(provider, provider)
            print(f"    {label:<12} {_green('configured')}")
    else:
        print(f"    {_dim('None configured. Use the app or POST /keys to add API keys.')}")

    # OAuth tokens
    tokens = _read_tokens(config.data_dir)
    if tokens:
        print()
        print("  OAuth:")
        for provider, token_data in sorted(tokens.items()):
            label = provider_labels.get(provider, provider)
            email = token_data.get("email", "")
            if email:
                print(f"    {label:<12} {_green('connected')} as {email}")
            else:
                print(f"    {label:<12} {_green('connected')}")

    print()
    print(f"  Data dir: {_dim(str(config.data_dir))}")
    print(f"  Version:  {VERSION}")

    return 0


def cmd_models(args: argparse.Namespace, config: RuntimeConfig) -> int:
    """List available models from all sources."""
    print(_bold("Available Models"))
    print()

    # Local models (Ollama)
    ollama_ok = _ollama_is_running(config.ollama_url)
    print("  Local (Ollama):")
    if ollama_ok:
        models = _ollama_list_models(config.ollama_url)
        if models:
            for m in models:
                print(f"    {m:<20} {_green('ready')}")
        else:
            print(f"    {_dim('No models pulled. Run: ollama pull llama3')}")
    else:
        print(f"    {_dim('Ollama not running. Start: ollama serve')}")

    # Cloud models (BYOK)
    print()
    print("  Cloud (BYOK):")

    keys = _read_keys(config.data_dir)
    cloud_providers = {
        "anthropic": [
            ("claude-sonnet-4-20250514", "Claude Sonnet 4"),
            ("claude-opus-4-20250514", "Claude Opus 4"),
            ("claude-haiku-3-5-20241022", "Claude Haiku 3.5"),
        ],
        "openai": [
            ("gpt-4o", "GPT-4o"),
            ("gpt-4o-mini", "GPT-4o Mini"),
            ("o3-mini", "o3 Mini"),
        ],
        "google": [
            ("gemini-2.0-flash", "Gemini 2.0 Flash"),
            ("gemini-2.5-pro", "Gemini 2.5 Pro"),
        ],
    }

    any_cloud = False
    for provider, model_list in cloud_providers.items():
        if provider in keys:
            any_cloud = True
            for model_id, label in model_list:
                print(f"    {label:<24} {_green('ready')}  {_dim(model_id)}")
        else:
            for _model_id, label in model_list:
                print(f"    {label:<24} {_red('no key')}  {_dim('POST /keys to add')}")

    if not any_cloud and not keys:
        print(f"    {_dim('No API keys configured.')}")

    return 0


def cmd_login(args: argparse.Namespace, config: RuntimeConfig) -> int:
    """Start OAuth flow for a cloud provider."""
    from runtime.auth.providers import PROVIDERS, get_provider

    provider_name = args.provider.lower()

    # Map common aliases
    aliases = {"claude": "anthropic", "chatgpt": "openai", "gpt": "openai", "gemini": "google"}
    provider_key = aliases.get(provider_name, provider_name)

    try:
        provider_config = get_provider(provider_key)
    except ValueError as e:
        print(f"{_red('Error')}: {e}")
        return 1

    client_id = provider_config.get_client_id()
    if not client_id:
        print(f"{_red('Error')}: No client ID configured for {provider_config.name}.")
        print(f"  Set the environment variable: {provider_config.client_id_env}")
        return 1

    # Build the authorization URL
    import hashlib
    import secrets

    state = secrets.token_urlsafe(32)
    callback_port = 9876
    redirect_uri = f"http://localhost:{callback_port}/callback"

    params = {
        "client_id": client_id,
        "redirect_uri": redirect_uri,
        "response_type": "code",
        "scope": " ".join(provider_config.scopes),
        "state": state,
    }
    if provider_config.extra_auth_params:
        params.update(provider_config.extra_auth_params)

    # PKCE if required
    code_verifier = None
    if provider_config.requires_pkce:
        code_verifier = secrets.token_urlsafe(64)
        digest = hashlib.sha256(code_verifier.encode("ascii")).digest()
        import base64
        code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
        params["code_challenge"] = code_challenge
        params["code_challenge_method"] = "S256"

    query = "&".join(f"{k}={urllib.request.quote(str(v))}" for k, v in params.items())
    auth_url = f"{provider_config.auth_url}?{query}"

    print(f"Connecting to {_bold(provider_config.name)}...")
    print(f"Opening browser for authorization...")
    print()

    # Start a temporary local server to receive the callback
    from http.server import BaseHTTPRequestHandler, HTTPServer
    from urllib.parse import parse_qs, urlparse

    auth_code = None
    received_state = None
    server_error = None

    class CallbackHandler(BaseHTTPRequestHandler):
        def do_GET(self) -> None:
            nonlocal auth_code, received_state, server_error
            parsed = urlparse(self.path)
            qs = parse_qs(parsed.query)

            if parsed.path == "/callback":
                auth_code = qs.get("code", [None])[0]
                received_state = qs.get("state", [None])[0]
                error = qs.get("error", [None])[0]

                if error:
                    server_error = qs.get("error_description", [error])[0]
                    self.send_response(200)
                    self.send_header("Content-Type", "text/html")
                    self.end_headers()
                    self.wfile.write(b"<html><body><h2>Authorization failed.</h2><p>You can close this tab.</p></body></html>")
                else:
                    self.send_response(200)
                    self.send_header("Content-Type", "text/html")
                    self.end_headers()
                    self.wfile.write(b"<html><body><h2>Connected to Brynq!</h2><p>You can close this tab.</p></body></html>")
            else:
                self.send_response(404)
                self.end_headers()

        def log_message(self, format: str, *log_args: Any) -> None:
            pass  # Suppress request logging

    try:
        server = HTTPServer(("127.0.0.1", callback_port), CallbackHandler)
    except OSError:
        print(f"{_red('Error')}: Port {callback_port} is in use. Close other OAuth flows and try again.")
        return 1

    server.timeout = 120  # 2 minute timeout

    # Open browser
    webbrowser.open(auth_url)
    print(f"Waiting for authorization (timeout: 2 minutes)...")
    print(f"  {_dim('If the browser did not open, visit:')}")
    print(f"  {_dim(auth_url[:100])}{'...' if len(auth_url) > 100 else ''}")
    print()

    # Wait for callback
    server.handle_request()
    server.server_close()

    if server_error:
        print(f"{_red('Error')}: Authorization failed: {server_error}")
        return 1

    if not auth_code:
        print(f"{_red('Error')}: No authorization code received. Try again.")
        return 1

    if received_state != state:
        print(f"{_red('Error')}: State mismatch. Possible CSRF attack. Aborting.")
        return 1

    # Exchange code for token
    print("Exchanging authorization code for token...", end="", flush=True)

    token_body: dict[str, str] = {
        "grant_type": "authorization_code",
        "code": auth_code,
        "redirect_uri": redirect_uri,
        "client_id": client_id,
    }
    if code_verifier:
        token_body["code_verifier"] = code_verifier

    token_data = json.dumps(token_body).encode("utf-8")
    token_req = urllib.request.Request(
        provider_config.token_url,
        data=token_data,
        headers={"Content-Type": "application/json"},
        method="POST",
    )

    try:
        with urllib.request.urlopen(token_req, timeout=30) as resp:
            token_resp = json.loads(resp.read().decode("utf-8"))
    except Exception as e:
        print(f" {_red('failed')}")
        print(f"Token exchange error: {e}")
        return 1

    print(f" {_green('done')}")

    # Store token
    tokens = _read_tokens(config.data_dir)
    tokens[provider_key] = {
        "access_token": token_resp.get("access_token", ""),
        "refresh_token": token_resp.get("refresh_token", ""),
        "token_type": token_resp.get("token_type", "bearer"),
        "scope": token_resp.get("scope", ""),
        "email": token_resp.get("email", ""),
        "connected_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
    }
    _write_tokens(config.data_dir, tokens)

    email = token_resp.get("email", "")
    if email:
        print(f"\n{_green('Connected')} to {provider_config.name} as {_bold(email)}")
    else:
        print(f"\n{_green('Connected')} to {provider_config.name}")

    return 0


def cmd_chat(args: argparse.Namespace, config: RuntimeConfig) -> int:
    """Interactive orchestrated multi-model chat.

    Auto-detects available models, classifies each request, picks the
    right strategy (single/debate/refine/fan-out), and routes to the
    right models. Code/action tasks go to Claude only. Thinking tasks
    use multi-model collaboration.

    With --once: non-interactive mode. Runs one prompt and exits with
    output to stdout. Used by autopilot.py for autonomous operation.
    """
    # Handle --clipboard: read prompt from clipboard if no positional prompt given
    if getattr(args, "clipboard", False) and not getattr(args, "prompt", []):
        from runtime.integrations.clipboard import read_clipboard
        print("Reading prompt from clipboard...")
        clip_text = read_clipboard().strip()
        if clip_text:
            args.prompt = clip_text.split()
        else:
            print("Clipboard is empty.", file=sys.stderr)
            return 1

    # Handle --once mode (non-interactive, for autopilot)
    if getattr(args, "once", False) and getattr(args, "prompt", []):
        prompt_text = " ".join(args.prompt)
        # Use Claude Code with tool use so it can actually edit files
        import subprocess as _sp
        try:
            system = (
                "You are running inside Brynq autopilot. Your job is to edit "
                "files in the project to complete the task. Do NOT just output "
                "code — use your tools to Read, Edit, and Write files directly. "
                "Keep changes minimal and focused. Run existing tests if unsure.\n\n"
                "CRITICAL: Before implementing anything, READ the target file and "
                "SEARCH for the feature. If it already exists and works, output DONE "
                "and make NO changes. If it exists but is incomplete, fix it — do NOT "
                "add a duplicate. Only create new code if the feature is truly missing."
            )
            # Load architectural guardrails if available
            directive_path = Path(__file__).resolve().parent.parent / "AUTOPILOT_DIRECTIVE.md"
            if directive_path.exists():
                try:
                    directive = directive_path.read_text(encoding="utf-8")
                    system = directive + "\n\n" + system
                except Exception:
                    pass
            full_prompt = f"{system}\n\nTask: {prompt_text}"
            r = _sp.run(
                ["claude", "-p", full_prompt, "--allowedTools",
                 "Read,Write,Edit,Glob,Grep,Bash"],
                capture_output=True, text=True, timeout=300,
                encoding="utf-8", errors="replace",
            )
            if r.returncode == 0:
                print(r.stdout.strip() if r.stdout.strip() else "DONE")
                return 0
            else:
                print(r.stderr.strip() if r.stderr else "[no output]", file=sys.stderr)
                return 1
        except Exception as e:
            print(f"[error: {e}]", file=sys.stderr)
            return 1
    verbose = getattr(args, "verbose", False)
    import re as _re
    import subprocess
    import threading
    import os
    # ── Cloud skill count (non-blocking fetch at startup) ─────────
    _cloud_status: dict[str, Any] = {"skill_count": None, "online": None}

    def _fetch_skill_count() -> None:
        """Fetch skill count from cloud API. Runs in background thread."""
        try:
            url = f"{config.cloud_url}/v1/skills/categories"
            req = urllib.request.Request(url, method="GET")
            with urllib.request.urlopen(req, timeout=5.0) as resp:
                data = json.loads(resp.read().decode("utf-8"))
                total = 0
                if isinstance(data, dict):
                    # Sum across categories: {"code": 1200, "research": 800, ...}
                    for v in data.values():
                        if isinstance(v, int):
                            total += v
                        elif isinstance(v, list):
                            total += len(v)
                    # Also accept a top-level "total" or "count" field
                    total = data.get("total", data.get("count", total))
                elif isinstance(data, list):
                    total = len(data)
                _cloud_status["skill_count"] = total if total > 0 else None
                _cloud_status["online"] = True
        except Exception:
            _cloud_status["skill_count"] = None
            _cloud_status["online"] = False

    _skill_thread = threading.Thread(target=_fetch_skill_count, daemon=True)
    _skill_thread.start()

    # ── Detect available models ────────────────────────────────────
    active_models: list[str] = []

    # Ensure npm global bin is on PATH (Windows installs CLIs here)
    _npm_global = Path.home() / "AppData" / "Roaming" / "npm"
    if sys.platform == "win32" and _npm_global.exists():
        _current_path = os.environ.get("PATH", "")
        if str(_npm_global) not in _current_path:
            os.environ["PATH"] = str(_npm_global) + os.pathsep + _current_path

    if shutil.which("claude") or shutil.which("claude.cmd"):
        active_models.append("claude")
    if shutil.which("gemini") or shutil.which("gemini.cmd"):
        active_models.append("gemini")
    if os.environ.get("OPENAI_API_KEY"):
        active_models.append("chatgpt")

    ollama_ok = _ollama_is_running(config.ollama_url)
    if ollama_ok:
        for m in _ollama_list_models(config.ollama_url):
            active_models.append(f"ollama:{m}")

    all_detected = list(active_models)  # snapshot for /add validation

    # ── Setup guide ─────────────────────────────────────────────
    has_claude = shutil.which("claude") is not None
    has_gemini = shutil.which("gemini") is not None
    has_ollama = ollama_ok

    has_npx = shutil.which("npx") is not None or shutil.which("npx.cmd") is not None
    has_node = shutil.which("node") is not None or shutil.which("node.cmd") is not None

    if not active_models:
        print()
        print(f"  {_bold('Welcome to Brynq!')}")
        print()
        print(f"  No AI models detected yet. Let me help you set up:")
        print()
        print(f"  {_bold('Easiest (free, runs locally):')}")
        print(f"    1. Install Ollama        https://ollama.com/download")
        print(f"    2. Pull a model          ollama pull llama3")
        print(f"    3. Restart Brynq         python -m brynq")
        print()
        print(f"  {_bold('Cloud (use your existing subscriptions):')}")
        print(f"    Claude Code              pip install claude-code")
        print(f"    Gemini CLI               npm install -g @google/gemini-cli")
        print(f"    ChatGPT                  set OPENAI_API_KEY=sk-your-key")
        print()
        if not has_node:
            print(f"  {_dim('Note: Node.js not found. Install from https://nodejs.org')}")
            print(f"  {_dim('Node.js is needed for marketplace tools and Gemini CLI.')}")
            print()
        print(f"  You can also chat without models — Brynq will use the cloud API.")
        print()
        # Don't exit — let them try chatting anyway

    # Show what's connected and what's missing
    missing: list[str] = []
    if not has_claude:
        missing.append("Claude Code")
    if not has_gemini:
        missing.append("Gemini CLI")
    if not has_ollama:
        missing.append("Ollama")

    if missing:
        # Show a brief suggestion after the header, not a blocker
        _missing_hint = missing  # save for header display
    else:
        _missing_hint = []

    # ── ANSI helpers ───────────────────────────────────────────────
    _MAGENTA = "\033[35m"
    _BLUE = "\033[34m"
    _YELLOW = "\033[33m"
    _GREEN_C = "\033[32m"
    _RED_C = "\033[31m"
    _WHITE_BOLD = "\033[1;37m"
    _DIM = "\033[2m"
    _BG_PANEL = "\033[48;5;236m"
    _BG_HEADER = "\033[44;37m"
    _RESET = "\033[0m"

    def _model_label(model: str) -> str:
        if model == "claude":
            name, color = "Claude (Opus 4.6)", _MAGENTA
        elif model == "gemini":
            name, color = "Gemini 3.1", _BLUE
        elif model.startswith("ollama:"):
            name, color = f"Ollama/{model.split(':', 1)[1]}", _YELLOW
        else:
            name, color = model, ""
        return f"{color}{name}{_RESET}" if _COLOR and color else name

    # ── Conversation memory ───────────────────────────────────────
    conversation: list[dict[str, str]] = []  # [{role, content}, ...]
    _rag_enabled = True  # RAG context injection (toggle with /rag on/off)

    # ── File auto-reading ─────────────────────────────────────────
    _MAX_FILE_SIZE = 500 * 1024  # 500 KB
    _FILE_PATH_RE = _re.compile(
        r"""(?:^|(?<=\s))"""               # start of string or preceded by whitespace
        r"""(?!https?://)"""               # not a URL
        r"""(?!/[a-z]+(?:\s|$))"""         # not a /slash command
        r"""("""
        r"""(?:[A-Za-z]:)?"""              # optional drive letter (Windows)
        r"""[^\s"'<>|*?]+"""              # path chars (no spaces, no shell metachars)
        r""")"""
        r"""(?=\s|$)""",                   # followed by whitespace or end
        _re.MULTILINE,
    )

    def _scan_file_paths(text: str) -> list[Path]:
        """Extract tokens from *text* that resolve to existing files under 500 KB."""
        found: list[Path] = []
        seen: set[str] = set()
        for m in _FILE_PATH_RE.finditer(text):
            token = m.group(1)
            # Must contain a path separator or a dot-extension to look like a file
            if "/" not in token and "\\" not in token and "." not in token:
                continue
            p = Path(token).expanduser()
            if not p.is_absolute():
                p = Path.cwd() / p
            resolved = str(p.resolve())
            if resolved in seen:
                continue
            seen.add(resolved)
            try:
                if p.is_file() and p.stat().st_size <= _MAX_FILE_SIZE:
                    found.append(p)
            except OSError:
                pass
        return found

    def _prepend_file_contents(text: str, files: list[Path]) -> str:
        """Return *text* with the contents of *files* prepended as context."""
        if not files:
            return text
        parts: list[str] = []
        for fp in files:
            try:
                content = fp.read_text(encoding="utf-8", errors="replace")
                parts.append(f"--- Contents of {fp.name} ---\n{content}\n--- End of {fp.name} ---")
            except OSError:
                pass
        if not parts:
            return text
        return "\n\n".join(parts) + f"\n\n{text}"

    # ── Direct action executor ─────────────────────────────────────
    def _try_direct_action(text: str) -> str | None:
        """Try to execute simple system actions directly.

        Returns the result message, or None if it's not a simple action.
        """
        t = text.lower().strip()

        # Create folder
        m = _re.search(r"(?:create|make|new)\s+(?:a\s+)?(?:folder|directory|dir)\s+(?:called|named)?\s*[\"']?(\S+)[\"']?", t)
        if not m:
            m = _re.search(r"(?:mkdir)\s+[\"']?(\S+)[\"']?", t)
        if m:
            folder_name = m.group(1).strip("\"'")
            target = Path.home() / "Desktop" / folder_name
            if "desktop" not in t:
                target = Path.cwd() / folder_name
            try:
                target.mkdir(parents=True, exist_ok=True)
                return f"Created folder: {target}"
            except OSError as e:
                return f"Failed to create folder: {e}"

        # Delete folder/file
        m = _re.search(r"(?:delete|remove)\s+(?:the\s+)?(?:folder|directory|file)\s+(?:called|named)?\s*[\"']?(\S+)[\"']?", t)
        if m:
            name = m.group(1).strip("\"'")
            target = Path.home() / "Desktop" / name if "desktop" in t else Path.cwd() / name
            if target.exists():
                if target.is_dir():
                    import shutil as _shutil
                    _shutil.rmtree(target)
                else:
                    target.unlink()
                return f"Deleted: {target}"
            return f"Not found: {target}"

        # Open URL
        m = _re.search(r"(?:open)\s+(https?://\S+)", t)
        if m:
            import webbrowser
            webbrowser.open(m.group(1))
            return f"Opened: {m.group(1)}"

        return None  # Not a simple action — route to models

    # ── Task classification ────────────────────────────────────────
    _CODE_MODELS = {"claude", "gemini"}

    def _classify_local(text: str) -> tuple[str, str, str]:
        t_lower = text.lower()
        if any(w in t_lower for w in ["brynq", "marketplace", "mcp", "agent", "tool"]) and "what" in t_lower:
            return "general", "single", "question about Brynq"
        
        """Local keyword-based classification (fallback).

        Returns:
            task_type: action, code, analysis, creative, research, compare, general
            strategy: single, debate, refine, fan_out
            reason: human-readable explanation of why this strategy was chosen
        """
        t = text.lower()

        # Action tasks — try direct execution first
        action_kw = (
            "create ", "delete ", "make ", "open ", "run ", "install ",
            "move ", "copy ", "rename ", "download ", "mkdir", "folder",
            "file ", "save ", "write to ", "execute",
        )
        matched = [k.strip() for k in action_kw if k in t]
        if matched:
            return "action", "single", f"action keywords matched: {matched}; routed to single model for direct execution"

        # Code tasks — detect complexity to pick strategy
        code_keywords = (
            "code", "function", "class ", "debug", "fix ", "refactor",
            "implement", "script", "program", "api ", "endpoint",
            "bug ", "error ", "test ", "deploy",
        )
        matched = [k.strip() for k in code_keywords if k in t]
        if matched:
            # Measure complexity: long prompts or multiple requirements = multi-model
            complexity = 0
            breakdown: list[str] = []
            length_score = len(t) // 200
            if length_score:
                complexity += length_score
                breakdown.append(f"length={length_score}")
            kw_score = len(matched)
            complexity += kw_score
            breakdown.append(f"keywords({kw_score})={matched}")
            and_count = t.count(" and ")
            if and_count:
                complexity += and_count
                breakdown.append(f"'and'×{and_count}")
            then_count = t.count(" then ")
            if then_count:
                complexity += then_count
                breakdown.append(f"'then'×{then_count}")
            comma_count = t.count(",")
            if comma_count:
                complexity += comma_count
                breakdown.append(f"commas×{comma_count}")
            if "mode" in t or "feature" in t:
                complexity += 1
                breakdown.append("mode/feature")
            if "self" in t or "itself" in t or "own source" in t:
                complexity += 1
                breakdown.append("self-referential")

            detail = "; ".join(breakdown)
            if complexity >= 12:
                return "code", "swarm", f"complexity={complexity} (>=12 → swarm); {detail}"
            elif complexity >= 8:
                return "code", "refine", f"complexity={complexity} (>=8 → refine: draft→critique→revise); {detail}"
            elif complexity >= 5:
                return "code", "fan_out", f"complexity={complexity} (>=5 → fan_out: parallel models); {detail}"
            else:
                return "code", "single", f"complexity={complexity} (<5 → single model); {detail}"

        # Compare/debate tasks
        compare_kw = (
            "compare", "vs ", "versus", "better", "pros and cons",
            "debate", "argue", "which ", "should i use",
            "difference between", "advantages",
        )
        matched = [k.strip() for k in compare_kw if k in t]
        if matched:
            return "compare", "debate", f"compare keywords matched: {matched}; multiple models will argue pro/con"

        # Creative tasks — draft → critique → revise
        creative_kw = (
            "write ", "draft ", "compose", "design ", "story",
            "email ", "blog ", "essay ", "article ", "landing page",
            "headline", "slogan", "pitch",
        )
        matched = [k.strip() for k in creative_kw if k in t]
        if matched:
            return "creative", "refine", f"creative keywords matched: {matched}; draft→critique→revise cycle"

        # Research/analysis — multiple perspectives
        research_kw = (
            "analyze", "research", "explain", "how does",
            "what is", "why ", "investigate", "explore",
            "summarize", "overview",
        )
        matched = [k.strip() for k in research_kw if k in t]
        if matched:
            return "research", "fan_out", f"research keywords matched: {matched}; fan-out to multiple models for perspectives"

        return "general", "single", "no category keywords matched; defaulting to general/single"

    def _classify_cloud(text: str) -> tuple[str, str, str] | None:
        """Try cloud-based classification via /v1/plan endpoint.

        Returns (task_type, strategy, reason) on success, or None if unreachable.
        """
        if _cloud_status["online"] is False:
            return None
        try:
            url = f"{config.cloud_url}/v1/plan"
            body = json.dumps({"input": text, "classify_only": True}).encode("utf-8")
            req = urllib.request.Request(
                url, data=body,
                headers={"Content-Type": "application/json"},
                method="POST",
            )
            with urllib.request.urlopen(req, timeout=3.0) as resp:
                data = json.loads(resp.read().decode("utf-8"))
                task_type = data.get("task_type", "").strip()
                strategy = data.get("strategy", "").strip()
                if task_type and strategy:
                    _cloud_status["online"] = True
                    reason = f"cloud /v1/plan classified as {task_type}/{strategy}"
                    cloud_reason = data.get("reason", "")
                    if cloud_reason:
                        reason += f": {cloud_reason}"
                    return task_type, strategy, reason
        except Exception:
            pass
        return None

    def _classify(text: str) -> tuple[str, str, str]:
        """Classify user input → (task_type, strategy, reason).

        Tries cloud /v1/plan first for full skill matching (7,889 skills),
        falls back to local keyword matching if cloud is unreachable.

        Returns:
            task_type: action, code, analysis, creative, research, compare, general
            strategy: single, debate, refine, fan_out
            reason: explanation of why this strategy was chosen
        """
        cloud_result = _classify_cloud(text)
        if cloud_result is not None:
            return cloud_result
        return _classify_local(text)

    def _pick_models(task_type: str, strategy: str) -> list[str]:
        """Pick which models to use based on task type."""
        # Apply learned preferences if available
        try:
            preferred = _prefs.get("preferred_model")
            if preferred and preferred in active_models:
                # Move preferred model to front of candidates
                reordered = [preferred] + [m for m in active_models if m != preferred]
            else:
                reordered = active_models
        except Exception:
            reordered = active_models

        code_capable = [m for m in reordered if m in _CODE_MODELS]
        all_m = reordered

        if task_type == "action":
            if "claude" in active_models:
                return ["claude"]
            return code_capable[:1] if code_capable else all_m[:1]

        if task_type == "code":
            if strategy == "single":
                if "claude" in active_models:
                    return ["claude"]
                return code_capable[:1] if code_capable else all_m[:1]
            elif strategy == "refine":
                drafter = "claude" if "claude" in active_models else (code_capable[0] if code_capable else all_m[0])
                critic = "gemini" if "gemini" in active_models else (all_m[1] if len(all_m) > 1 else drafter)
                return [drafter, critic, drafter]
            elif strategy == "swarm":
                # Heavy tasks: all available models, repeated for parallelism
                # Scale up to 20 agents based on available models
                pool = code_capable or all_m
                agents = []
                while len(agents) < 20 and len(agents) < len(pool) * 4:
                    agents.extend(pool)
                return agents[:20]
            else:
                # Fan-out: all code-capable models in parallel
                return (code_capable or all_m)[:len(all_m)]

        if strategy == "debate":
            # Need at least 2 different models + a judge
            if len(all_m) >= 3:
                return all_m[:3]
            return all_m[:2] if len(all_m) >= 2 else all_m[:1]

        if strategy == "refine":
            # drafter (fast) + critic (smart) + reviser (fast)
            fast = [m for m in all_m if m.startswith("ollama:")] or all_m[-1:]
            smart = code_capable[:1] or all_m[:1]
            return [fast[0], smart[0], fast[0]]

        if strategy == "fan_out":
            # Up to 3 different models
            return all_m[:3]

        # Single — best available
        return code_capable[:1] if code_capable else all_m[:1]

    # ── System prompt (tells models they're inside Brynq) ─────────
    _BRYNQ_SYSTEM = (
        "You are running inside Brynq, an AI orchestration platform on the user's computer. "
        "You are one of several AI models working together. The user's data stays on their machine. "
        "Brynq can chain multiple models using these strategies: "
        "Sequential (A->B->C with context passing), "
        "Fan-Out (models run in parallel, results merged), "
        "Debate (two models argue, a third judges), "
        "Refine (one drafts, another critiques, first revises), "
        "or Single-Model (one model, simple tasks). "
        "The user can trigger chains with /chain or the chain builder. "
        "Answer the user's question directly. Do not offer to build Brynq or suggest the user "
        "install tools — they already have everything set up."
    )

    # ── Model runners ──────────────────────────────────────────────
    def _call_claude(prompt: str) -> str:
        # Build context from last 3 conversation turns (shorter = faster)
        context_parts = []
        for msg in conversation[-6:]:
            role = "User" if msg["role"] == "user" else "Assistant"
            context_parts.append(f"{role}: {msg['content'][:300]}")

        system_prefix = f"[System: {_BRYNQ_SYSTEM}]\n\n"
        if context_parts:
            full_prompt = system_prefix + "Previous conversation:\n" + "\n".join(context_parts) + f"\n\nUser: {prompt}"
        else:
            full_prompt = system_prefix + prompt

        # Try direct Anthropic API first (fastest, ~2-3s)
        api_key = os.environ.get("ANTHROPIC_API_KEY", "")
        if api_key:
            try:
                body = json.dumps({
                    "model": "claude-sonnet-4-6-20250514",
                    "max_tokens": 4096,
                    "messages": [{"role": "user", "content": full_prompt}],
                }).encode("utf-8")
                req = urllib.request.Request(
                    "https://api.anthropic.com/v1/messages",
                    data=body,
                    headers={
                        "Content-Type": "application/json",
                        "x-api-key": api_key,
                        "anthropic-version": "2023-06-01",
                    },
                )
                with urllib.request.urlopen(req, timeout=600) as resp:
                    result = json.loads(resp.read().decode("utf-8"))
                    return result.get("content", [{}])[0].get("text", "")
            except Exception:
                pass  # Fall through to CLI

        # Fallback: claude -p (try with tools first, plain if that fails)
        for cmd_args in [
            ["claude", "-p", full_prompt, "--allowedTools", "Read,Write,Edit,Glob,Grep,Bash"],
            ["claude", "-p", full_prompt],
        ]:
            try:
                r = subprocess.run(
                    cmd_args,
                    capture_output=True, text=True, timeout=None,
                    encoding="utf-8", errors="replace",
                )
                if r.returncode == 0 and r.stdout.strip():
                    return r.stdout.strip()
                if r.returncode != 0 and "--allowedTools" in cmd_args:
                    continue  # Try without tools
                if r.returncode != 0:
                    return f"[error: {r.stderr.strip()[:200]}]"
            except FileNotFoundError:
                return "[Claude CLI not found. Install it or set ANTHROPIC_API_KEY]"
            except subprocess.TimeoutExpired:
                return "[timed out — try a shorter prompt]"
            except Exception:
                if "--allowedTools" in cmd_args:
                    continue  # Try without tools
                return "[error calling Claude]"
        return "[Claude did not respond]"

    def _call_gemini(prompt: str) -> str:
        full_prompt = f"[System: {_BRYNQ_SYSTEM}]\n\n{prompt}"
        # Windows needs .cmd extension for npm-installed CLIs
        gemini_cmd = "gemini.cmd" if sys.platform == "win32" else "gemini"
        for cmd_args in [
            [gemini_cmd, "-p", full_prompt, "--allowedTools", "Read,Write,Edit,Glob,Grep,Bash"],
            [gemini_cmd, "-p", full_prompt],
        ]:
            try:
                r = subprocess.run(
                    cmd_args, input=full_prompt,
                    capture_output=True, text=True, timeout=None,
                    encoding="utf-8", errors="replace",
                )
                if r.returncode == 0 and r.stdout.strip():
                    return r.stdout.strip()
                if r.returncode != 0 and "--allowedTools" in cmd_args:
                    continue
                if r.returncode != 0:
                    return f"[error: {r.stderr.strip()[:200]}]"
            except FileNotFoundError:
                return "[Gemini CLI not found. Install: npm install -g @google/gemini-cli]"
            except subprocess.TimeoutExpired:
                return "[timed out — try a shorter prompt]"
            except Exception:
                if "--allowedTools" in cmd_args:
                    continue
                return "[error calling Gemini]"
        return "[Gemini did not respond]"

    def _call_ollama(prompt: str, model: str) -> str:
        try:
            body = {
                "model": model,
                "messages": [
                    {"role": "system", "content": _BRYNQ_SYSTEM},
                    {"role": "user", "content": prompt},
                ],
                "stream": False,
            }
            data = json.dumps(body).encode("utf-8")
            req = urllib.request.Request(
                f"{config.ollama_url}/api/chat",
                data=data,
                headers={"Content-Type": "application/json"},
                method="POST",
            )
            with urllib.request.urlopen(req, timeout=config.ollama_timeout) as resp:
                result = json.loads(resp.read().decode("utf-8"))
                return result.get("message", {}).get("content", "")
        except urllib.error.HTTPError as e:
            if e.code == 404:
                # Model not found — list available models
                available = _list_ollama_models()
                if available:
                    names = ", ".join(available)
                    return f"[error: model '{model}' not found. Available models: {names}]"
                return f"[error: model '{model}' not found. Pull it with: ollama pull {model}]"
            return f"[error: {e}]"
        except urllib.error.URLError as e:
            reason = getattr(e, "reason", None)
            if isinstance(reason, ConnectionRefusedError) or (
                isinstance(reason, OSError) and "refused" in str(reason).lower()
            ):
                return "[error: Ollama is not running. Start it with: ollama serve]"
            return f"[error: {e}]"
        except Exception as e:
            return f"[error: {e}]"

    def _list_ollama_models() -> list[str]:
        """Fetch available model names from Ollama's /api/tags endpoint."""
        try:
            req = urllib.request.Request(
                f"{config.ollama_url}/api/tags",
                method="GET",
            )
            with urllib.request.urlopen(req, timeout=5) as resp:
                data = json.loads(resp.read().decode("utf-8"))
                return [m.get("name", "") for m in data.get("models", []) if m.get("name")]
        except Exception:
            return []

    def _call_model(model: str, prompt: str) -> str:
        # =========================================================
        # THE OMNI-PIPELINE (V36-V45 INTEGRATION)
        # =========================================================
        final_response = ""
        is_cached = False
        scrub_mapping = {}
        processed_prompt = prompt
        
        # 1. Empathy Layer (Cadence Adjuster)
        try:
            from runtime.cognition.empathy import UserStateDetector
            from runtime.cognition.cadence import CadenceAdjuster
            history_text = " ".join([c for _, _, c, _ in chat_history]) if chat_history else ""
            detector = UserStateDetector()
            state = detector.analyze_sentiment(history_text)
            adjuster = CadenceAdjuster()
            processed_prompt = adjuster.modify_prompt(processed_prompt, state)
        except Exception:
            pass

        # 2. Privacy Enclave (PII Scrubber - Outbound)
        try:
            from runtime.security.pii_scrubber import PIIScrubber
            scrubber = PIIScrubber()
            processed_prompt, scrub_mapping = scrubber.anonymize(processed_prompt)
        except Exception:
            scrubber = None

        # 3. Neural Cache Check
        emb = None
        try:
            from runtime.memory.neural_cache import SemanticCache
            import numpy as np
            if not hasattr(sys, "_neural_cache"):
                sys._neural_cache = SemanticCache()

            # Dummy embedding logic for MVP integration
            emb = np.random.rand(128)
            cached_res = sys._neural_cache.check_cache(emb, threshold=0.95)
            if cached_res:
                final_response = cached_res
                is_cached = True
        except Exception:
            pass

        # 4. Core Execution (If not cached)
        if not is_cached:
            if model == "claude":
                result = _call_claude(processed_prompt)
            elif model == "gemini":
                result = _call_gemini(processed_prompt)
            elif model.startswith("ollama:"):
                result = _call_ollama(processed_prompt, model.split(":", 1)[1])
            elif model == "brynq-coder-v1":
                try:
                    from runtime.models.brynq_native import BrynqNativeAdapter
                    if not hasattr(sys, "_brynq_native"):
                        sys._brynq_native = BrynqNativeAdapter()
                    result = sys._brynq_native.generate_response(processed_prompt)
                except Exception as e:
                    result = f"[Brynq Native Error: {e}]"
            else:
                return "[unknown model]"
                
            if not result or not result.strip():
                return f"[No response from {model}. Try rephrasing or using a different model.]"
            final_response = result

            # 5. Neural Cache Store
            try:
                if emb is not None and hasattr(sys, "_neural_cache"):
                    sys._neural_cache.store_cache(emb, final_response)
            except Exception:
                pass

        # 6. Privacy Enclave (PII Restorer - Inbound)
        if scrubber and scrub_mapping:
            try:
                final_response = scrubber.deanonymize(final_response, scrub_mapping)
            except Exception:
                pass

        # 7. Financial Ops (Agent Metering)
        try:
            from runtime.billing.metering import AgentMeter
            if not hasattr(sys, "_agent_meter"):
                sys._agent_meter = AgentMeter()
            # Rough token estimate
            tokens = len(prompt) // 4 + len(final_response) // 4
            sys._agent_meter.track_usage(model, time_ms=1000, tokens=tokens)
        except Exception:
            pass

        return final_response

    # ── Self-awareness: Brynq answers questions about itself ───────
    def _answer_about_brynq(text: str, cfg, models: list[str]) -> str | None:
        """If the user asks about Brynq's own capabilities, answer directly."""
        t = text.lower().strip()

        # Load marketplace stats lazily
        def _marketplace_stats() -> dict:
            return _get_catalog_and_stats()[1]

        def _installed_agents() -> list:
            return _get_installed_agents(cfg.data_dir)

        def _load_catalog():
            """Return a MarketplaceCatalog instance or None."""
            return _get_catalog_and_stats()[0]

        # Keyword marketplace search (e.g. "search marketplace for pdf", "find tool for csv")
        import re as _re
        _search_m = _re.match(
            r"(?:search\s+(?:marketplace|catalog|tools|agents)"
            r"|find\s+(?:a\s+)?(?:tool|agent|mcp)s?\s+for)"
            r"\s+(.+)",
            t,
        )
        if _search_m:
            keyword = _search_m.group(1).strip()
            cat = _load_catalog()
            if cat is not None:
                results = cat.search(keyword, limit=5)
                if results:
                    lines = [f"Top {len(results)} results for '{keyword}':"]
                    for i, e in enumerate(results, 1):
                        stars = f" ({e.rating:.1f}*)" if e.rating else ""
                        lines.append(f"  {i}. {e.name} — {e.description}{stars}")
                    return "\n".join(lines)
            return f"No marketplace results for '{keyword}'."

        # Category-specific tool/agent search (e.g. "what code tools", "show me data tools")
        _CATEGORY_NAMES = {
            "financial", "legal", "code", "research", "creative",
            "data", "productivity", "education", "other",
        }
        _cat_match = None
        for _cn in _CATEGORY_NAMES:
            if _cn + " tool" in t or _cn + " agent" in t or _cn + " mcp" in t:
                _cat_match = _cn
                break
        if _cat_match is not None:
            try:
                from cloud.marketplace.catalog import MarketplaceCatalog
                cat = None
                for data_path in [
                    Path(__file__).resolve().parent.parent / "cloud" / "data",
                    Path.cwd() / "cloud" / "data",
                    Path("cloud/data"),
                ]:
                    catalog_file = data_path / "marketplace" / "catalog.json"
                    if catalog_file.exists():
                        cat = MarketplaceCatalog(data_path)
                        break
                if cat is not None:
                    results = cat.search("", category=_cat_match, limit=5)
                    if results:
                        lines = [f"Top {len(results)} {_cat_match} tools:"]
                        for i, e in enumerate(results, 1):
                            stars = f" ({e.rating:.1f}*)" if e.rating else ""
                            lines.append(f"  {i}. {e.name} — {e.description}{stars}")
                        return "\n".join(lines)
                return f"No {_cat_match} tools found in the marketplace yet."
            except Exception:
                return f"No {_cat_match} tools found in the marketplace yet."

        # MCP / marketplace / tools questions

        # Check installed agents
        if "installed" in t or "what have i installed" in t:
            agents = _installed_agents()
            if not agents:
                return "You have no agents installed yet. Try /marketplace to find some!"
            lines = ["Installed Agents:"]
            for a in agents:
                name = a.get('name', 'Unknown')
                atype = a.get('type', 'mcp')
                install_date = a.get('install_date', 'Unknown')
                lines.append(f"  - {name} ({atype}) - Installed: {install_date}")
            return "\n".join(lines)

        # Help / Quickstart
        if "help me" in t or "what can i do" in t:
            return "Brynq Quick-Start:\n1. Just type your question to chat.\n2. Use /chain <query> to run an agent chain.\n3. Use /marketplace to discover tools.\n4. Use /install <agent-id> to add a tool.\n5. Use /help to see all commands."

        # Status
        if "status" in t or "system status" in t:
            active = ", ".join(models) if models else "None"
            agent_count = len(_installed_agents())
            return f"Brynq System Status:\n- Active Models: {active}\n- Installed Agents: {agent_count}\n- Cloud Connection: OK (via marketplace catalog)\n- Ollama: OK"

        if any(k in t for k in ["how many mcp", "how many tool", "how many agent",
                                 "marketplace", "what tools", "what agents",
                                 "list mcp", "list tool", "list agent",
                                 "available tool", "available agent"]):
            stats = _marketplace_stats()
            total = stats.get("total_entries", 0)
            cats = stats.get("by_category", {})
            installed = _installed_agents()
            lines = [f"Brynq marketplace: {total:,} tools available."]
            if cats:
                breakdown = ", ".join(f"{c}: {n}" for c, n in sorted(cats.items(), key=lambda x: -x[1]))
                lines.append(f"Categories: {breakdown}")
            if installed:
                lines.append(f"Installed on this machine: {len(installed)} ({', '.join(a.get('name', a.get('agent_id', '?')) for a in installed[:10])})")
            else:
                lines.append("None installed yet. Browse at /marketplace or brynq.ai/marketplace.")
            return "\n".join(lines)

        # Installed agents questions
        if any(k in t for k in ["installed", "what have i installed"]):
            installed = _installed_agents()
            if not installed:
                return "No agents installed yet. Browse at /marketplace or brynq.ai/marketplace."
            lines = [f"Installed agents ({len(installed)}):"]
            for a in installed:
                name = a.get("name", a.get("agent_id", "unknown"))
                atype = a.get("type", "unknown")
                date = a.get("install_date", a.get("installed_at", "unknown"))
                lines.append(f"  - {name} (type: {atype}, installed: {date})")
            return "\n".join(lines)

        # Status / system status
        if any(k in t for k in ["system status", "status"]):
            lines = ["Brynq system status:"]

            # Cloud connection
            try:
                url = f"{cfg.cloud_url}/health"
                req = urllib.request.Request(url, method="GET")
                with urllib.request.urlopen(req, timeout=5.0):
                    lines.append(f"  Cloud:    connected ({cfg.cloud_url})")
            except Exception:
                lines.append(f"  Cloud:    unreachable ({cfg.cloud_url})")

            # Ollama
            ollama_url = getattr(cfg, "ollama_url", "http://localhost:11434")
            if _ollama_is_running(ollama_url):
                ollama_models = _ollama_list_models(ollama_url)
                lines.append(f"  Ollama:   running ({len(ollama_models)} model{'s' if len(ollama_models) != 1 else ''})")
            else:
                lines.append("  Ollama:   stopped")

            # Installed agents
            installed = _installed_agents()
            if installed:
                names = ", ".join(
                    a.get("name", a.get("agent_id", "?")) for a in installed[:10]
                )
                lines.append(f"  Agents:   {len(installed)} installed ({names})")
            else:
                lines.append("  Agents:   none installed")

            # Active models
            lines.append(f"  Models:   {len(models)} active")
            for m in models:
                lines.append(f"    - {m}")

            return "\n".join(lines)

        # Model questions
        if any(k in t for k in ["how many model", "what model", "which model",
                                 "list model", "available model", "supported model"]):
            lines = [f"Active models: {len(models)}"]
            for m in models:
                lines.append(f"  - {m}")
            return "\n".join(lines)

        # Strategy questions
        if any(k in t for k in ["what strateg", "how many strateg", "chain strateg",
                                 "orchestrat", "how do you work", "how does brynq work"]):
            return (
                "Brynq uses 5 orchestration strategies:\n"
                "  1. Sequential — A->B->C with context passing\n"
                "  2. Fan-Out — models run in parallel, results merged\n"
                "  3. Debate — two models argue, a third judges\n"
                "  4. Refine — one drafts, another critiques, first revises\n"
                "  5. Single — one model for simple tasks\n\n"
                "Brynq picks the strategy automatically based on your prompt, "
                "or you can force one with /chain."
            )

        # Quick-start guide
        if any(k in t for k in ["help me", "what can i do", "how do i start",
                                 "getting started", "quick start", "quickstart"]):
            return (
                "Quick-start guide:\n\n"
                "  Chat      — just type a message and press Enter\n"
                "  Chain     — brynq-runtime chain \"summarize this with Claude then Gemini\"\n"
                "             or type /chain in chat to build a multi-model chain\n"
                "  Marketplace — type /marketplace to browse available agents and tools\n"
                "  Install   — brynq-runtime install <agent-name> to add an agent\n"
                "             or use /marketplace install <name> in chat\n"
                "  Models    — brynq-runtime models to see what's available\n"
                "  Status    — brynq-runtime status to check everything\n\n"
                "Tip: Ollama models work offline and free. Add Claude or Gemini\n"
                "via brynq-runtime login for more power."
            )

        # What is Brynq
        if any(k in t for k in ["what is brynq", "what are you", "who are you",
                                 "tell me about brynq", "about brynq", "what can you do"]):
            stats = _marketplace_stats()
            total = stats.get("total_entries", 0)
            return (
                f"Brynq is the operating system for AI workforces.\n\n"
                f"  Models: {len(models)} active ({', '.join(m.split('/')[-1].split(':')[0] for m in models)})\n"
                f"  Marketplace: {total:,} tools (MCP servers, agents, builtins)\n"
                f"  Strategies: Sequential, Fan-Out, Debate, Refine, Single\n"
                f"  Privacy: your data stays on this machine\n"
                f"  BYOK: uses your existing AI subscriptions\n\n"
                f"Type /help for commands, or just ask a question."
            )

        # Version
        if any(k in t for k in ["version", "what version"]):
            try:
                from runtime.version import __version__
                return f"Brynq v{__version__}"
            except Exception:
                return "Brynq v0.1.0"

        return None

    # ── Learning Subsystem Integration (optional — excluded from pip package) ──
    import uuid
    try:
        from runtime.learning.usage_tracker import UsageTracker
        from runtime.learning.preferences import Preferences
        from runtime.learning.session_analytics import SessionAnalytics
        _tracker = UsageTracker(config.data_dir)
        _prefs = Preferences(config.data_dir)
        _session_analytics = SessionAnalytics(_tracker)
        _current_session_id = _session_analytics.current_session_id()
        _learning_available = True
    except ImportError:
        _tracker = None
        _prefs = None
        _session_analytics = None
        _current_session_id = str(uuid.uuid4())[:8]
        _learning_available = False
    
    def _track_interaction(model: str, strategy: str, prompt: str, result: str, elapsed: float, success: bool):
        if not _tracker:
            return
        # Estimate tokens roughly
        p_tokens = len(prompt) // 4
        r_tokens = len(result) // 4
        _tracker.log(
            model=model, 
            strategy=strategy, 
            prompt_tokens=p_tokens, 
            response_tokens=r_tokens, 
            latency_ms=int(elapsed * 1000), 
            status="success" if success else "error",
            session_id=_current_session_id
        )

    # ── Strategy executors ─────────────────────────────────────────
    def _run_single(user_input: str, models: list[str]) -> None:
        model = models[0]
        result, elapsed = _call_model_with_spinner(model, user_input)
        _last_timer.clear()
        _last_timer.append((model, elapsed))
        chat_history.append(("assistant", _model_label(model), result, f"{elapsed:.1f}s"))

    def _run_debate(user_input: str, models: list[str]) -> None:
        model_a = models[0]
        model_b = models[1] if len(models) > 1 else models[0]
        judge = models[2] if len(models) > 2 else models[0]
        _last_timer.clear()

        # Pro
        pro, elapsed = _call_model_with_spinner(model_a, f"Present a STRONG CASE IN FAVOR. Best arguments and reasoning.\n\nTopic: {user_input}")
        _last_timer.append((model_a, elapsed))
        chat_history.append(("assistant", f"{_GREEN_C}PRO{_RESET} {_model_label(model_a)}" if _COLOR else f"PRO {_model_label(model_a)}", pro, f"{elapsed:.1f}s"))
        _redraw()

        # Con
        con, elapsed = _call_model_with_spinner(model_b, f"Present a STRONG CASE AGAINST. Weaknesses and counterarguments.\n\nTopic: {user_input}")
        _last_timer.append((model_b, elapsed))
        chat_history.append(("assistant", f"{_RED_C}CON{_RESET} {_model_label(model_b)}" if _COLOR else f"CON {_model_label(model_b)}", con, f"{elapsed:.1f}s"))
        _redraw()

        # Judge
        verdict, elapsed = _call_model_with_spinner(judge,
            f"Two arguments on: {user_input}\n\n=== PRO ===\n{pro}\n\n=== CON ===\n{con}\n\n"
            f"Evaluate fairly. Strongest points from each. Clear verdict."
        )
        _last_timer.append((judge, elapsed))
        chat_history.append(("assistant", f"VERDICT {_model_label(judge)}", verdict, f"{elapsed:.1f}s"))

    def _run_refine(user_input: str, models: list[str]) -> None:
        drafter = models[0]
        critic = models[1] if len(models) > 1 else models[0]
        reviser = models[2] if len(models) > 2 else models[0]
        _last_timer.clear()

        # Draft
        draft, elapsed = _call_model_with_spinner(drafter, f"Produce a solid first draft.\n\n{user_input}")
        _last_timer.append((drafter, elapsed))
        chat_history.append(("assistant", f"DRAFT {_model_label(drafter)}", draft, f"{elapsed:.1f}s"))
        _redraw()

        # Critique
        critique, elapsed = _call_model_with_spinner(critic,
            f"Review critically. Strengths, weaknesses, specific improvements.\n\nRequest: {user_input}\n\nDraft:\n{draft}"
        )
        _last_timer.append((critic, elapsed))
        chat_history.append(("assistant", f"CRITIQUE {_model_label(critic)}", critique, f"{elapsed:.1f}s"))
        _redraw()

        # Revise
        revision, elapsed = _call_model_with_spinner(reviser,
            f"Revise incorporating the critique. Produce an improved version.\n\nRequest: {user_input}\n\nDraft:\n{draft}\n\nCritique:\n{critique}"
        )
        _last_timer.append((reviser, elapsed))
        chat_history.append(("assistant", f"FINAL {_model_label(reviser)}", revision, f"{elapsed:.1f}s"))

    def _run_fan_out(user_input: str, models: list[str]) -> None:
        results_map: dict[str, str] = {}
        timings_map: dict[str, float] = {}
        lock = threading.Lock()
        _last_timer.clear()

        model_names = ", ".join(m.split("/")[-1].split(":")[0] for m in models)
        print(f"  {', '.join(_model_label(m) for m in models)} are typing...")
        spinner = _Spinner(f"Fan-out to {len(models)} models ({model_names}) ")
        spinner.start()

        def _threaded(m: str) -> None:
            s = time.time()
            out = _call_model(m, user_input)
            with lock:
                results_map[m] = out
                timings_map[m] = time.time() - s

        threads = [threading.Thread(target=_threaded, args=(m,), daemon=True) for m in models]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        spinner.stop()

        for m in models:
            _last_timer.append((m, timings_map.get(m, 0)))
            chat_history.append(("assistant", _model_label(m), results_map.get(m, ""), f"{timings_map.get(m,0):.1f}s"))

        # Synthesize if 2+ models
        if len(models) >= 2:
            _redraw()
            synth_model = "claude" if "claude" in active_models else models[0]
            synthesis, elapsed = _call_model_with_spinner(synth_model,
                f"Multiple perspectives on: {user_input}\n\n"
                + "\n\n".join(f"=== {m} ===\n{results_map.get(m,'')}" for m in models)
                + "\n\nSynthesize into one coherent response. Best insights from each."
            )
            _last_timer.append((synth_model, elapsed))
            chat_history.append(("assistant", f"SYNTHESIS {_model_label(synth_model)}", synthesis, f"{elapsed:.1f}s"))

    def _run_swarm(user_input: str, models: list[str]) -> None:
        """Run up to 20 agents in parallel, each tackling a piece of the task."""
        # Break task into subtasks for each agent
        num_agents = len(models)
        results_map: dict[int, str] = {}
        timings_map: dict[int, float] = {}
        lock = threading.Lock()
        _last_timer.clear()

        agent_names = [f"{models[i % len(set(models))].split('/')[-1].split(':')[0]}-{i+1}" for i in range(num_agents)]
        unique_models = list(dict.fromkeys(models))
        print(f"  Swarm: {', '.join(_model_label(m) for m in unique_models)} ({num_agents} agents) are typing...")
        spinner = _Spinner(f"Swarm: {num_agents} agents working ")
        spinner.start()

        def _agent_work(idx: int, model: str) -> None:
            role = ["architect", "implementer", "tester", "reviewer", "optimizer",
                    "documenter", "security", "refactorer", "debugger", "integrator",
                    "validator", "planner", "analyzer", "builder", "checker",
                    "designer", "evaluator", "formatter", "generator", "inspector"][idx % 20]
            prompt = (
                f"You are agent #{idx+1} ({role}) in a swarm of {num_agents} agents.\n"
                f"Your specific role: {role}. Focus ONLY on the {role} aspect.\n"
                f"Be concise — other agents handle other aspects.\n\n"
                f"Task: {user_input}"
            )
            s = time.time()
            out = _call_model(model, prompt)
            with lock:
                results_map[idx] = out
                timings_map[idx] = time.time() - s

        threads = [
            threading.Thread(target=_agent_work, args=(i, models[i]), daemon=True)
            for i in range(num_agents)
        ]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        spinner.stop()

        # Show each agent's contribution
        for i in range(num_agents):
            role = ["architect", "implementer", "tester", "reviewer", "optimizer",
                    "documenter", "security", "refactorer", "debugger", "integrator",
                    "validator", "planner", "analyzer", "builder", "checker",
                    "designer", "evaluator", "formatter", "generator", "inspector"][i % 20]
            label = f"AGENT-{i+1} ({role}) {_model_label(models[i])}"
            _last_timer.append((models[i], timings_map.get(i, 0)))
            chat_history.append(("assistant", label, results_map.get(i, ""), f"{timings_map.get(i,0):.1f}s"))

        # Synthesize all contributions
        _redraw()
        synth_model = "claude" if "claude" in active_models else models[0]
        all_parts = "\n\n".join(
            f"=== Agent {i+1} ({['architect','implementer','tester','reviewer','optimizer','documenter','security','refactorer','debugger','integrator','validator','planner','analyzer','builder','checker','designer','evaluator','formatter','generator','inspector'][i%20]}) ===\n{results_map.get(i,'')}"
            for i in range(num_agents)
        )
        synthesis, elapsed = _call_model_with_spinner(synth_model,
            f"You are the lead synthesizer. {num_agents} agents worked on this task in parallel.\n"
            f"Combine their work into ONE coherent, complete solution.\n\n"
            f"Original task: {user_input}\n\n{all_parts}\n\n"
            f"Produce the final unified output. Remove duplicates, resolve conflicts, keep the best from each."
        )
        _last_timer.append((synth_model, elapsed))
        chat_history.append(("assistant", f"SYNTHESIS {_model_label(synth_model)}", synthesis, f"{elapsed:.1f}s"))

    def _auto_save_code(history: list) -> None:
        """Auto-save code blocks from model output to files. No permissions needed."""
        import re as _re
        output_dir = Path("brynq_output")
        saved_files = []

        # Collect all output from this round
        all_output = "\n\n".join(entry[2] for entry in history if entry[0] == "assistant")

        # Find code blocks with filenames (```python filename.py or ```filename.py)
        # Pattern: ```lang\n...code...\n```
        code_blocks = _re.findall(r"```(\w*)\n(.*?)```", all_output, _re.DOTALL)

        if not code_blocks:
            return

        # Save the full synthesis/output as a markdown file
        output_dir.mkdir(exist_ok=True)
        timestamp = int(time.time())

        # Save each code block
        for i, (lang, code) in enumerate(code_blocks):
            if len(code.strip()) < 20:
                continue  # Skip tiny snippets

            ext = {"python": ".py", "py": ".py", "javascript": ".js", "js": ".js",
                   "typescript": ".ts", "ts": ".ts", "go": ".go", "rust": ".rs",
                   "bash": ".sh", "sh": ".sh", "sql": ".sql", "json": ".json",
                   "yaml": ".yaml", "yml": ".yaml", "html": ".html", "css": ".css",
                   }.get(lang.lower(), ".txt")

            filename = f"brynq_{timestamp}_{i+1}{ext}"
            filepath = output_dir / filename
            filepath.write_text(code.strip(), encoding="utf-8")
            saved_files.append(str(filepath))

        # Also save the full conversation as markdown
        md_path = output_dir / f"brynq_{timestamp}_full.md"
        md_content = []
        for entry in history:
            if entry[0] == "assistant":
                md_content.append(f"## {entry[1]}\n\n{entry[2]}\n")
        md_path.write_text("\n---\n\n".join(md_content), encoding="utf-8")
        saved_files.append(str(md_path))

        if saved_files:
            print(f"\n  {_dim(f'Auto-saved {len(saved_files)} files to {output_dir}/')}")
            for f in saved_files[:5]:
                print(f"    {_dim(f)}")
            if len(saved_files) > 5:
                print(f"    {_dim(f'...and {len(saved_files) - 5} more')}")
            print()

    # ── Terminal drawing ───────────────────────────────────────────
    term = shutil.get_terminal_size((80, 24))
    cols = term.columns

    def _bar(char: str = "─") -> str:
        return f"{_DIM}{char * cols}{_RESET}" if _COLOR else char * cols

    def _draw_header() -> None:
        sys.stdout.write("\033[2J\033[H")
        title = " BRYNQ CHAT "
        pad = max(0, (cols - len(title)) // 2)
        right = max(0, cols - pad - len(title))
        if _COLOR:
            print(f"{_BG_HEADER}{'─' * pad}{title}{'─' * right}{_RESET}")
        else:
            print(f"{'─' * pad}{title}{'─' * right}")
        model_tags = "  ".join(_model_label(m) for m in active_models)
        print(f"  {_DIM}Models:{_RESET} {model_tags}")
        print(f"  {_DIM}Orchestrated: action→Claude | compare→debate | creative→refine | research→fan-out{_RESET}")
        # Show cloud skill count or offline status
        if _cloud_status["online"] is True and _cloud_status["skill_count"]:
            count = f"{_cloud_status['skill_count']:,}"
            if _COLOR:
                print(f"  {_GREEN_C}{count} skills loaded{_RESET} {_DIM}| Connected to {config.cloud_url.replace('https://', '')}{_RESET}")
            else:
                print(f"  {count} skills loaded | Connected to {config.cloud_url.replace('https://', '')}")
        elif _cloud_status["online"] is False:
            if _COLOR:
                print(f"  {_YELLOW}Offline mode{_RESET} {_DIM}| Local classification only{_RESET}")
            else:
                print(f"  Offline mode | Local classification only")
        # else: still loading, don't show anything yet
        if _missing_hint:
            installs = {
                "Claude Code": "npm i -g @anthropic-ai/claude-code",
                "Gemini CLI": "npm i -g @google/gemini-cli",
                "Ollama": "https://ollama.com/download",
            }
            tips = "  ".join(f"{n}: {installs[n]}" for n in _missing_hint if n in installs)
            print(f"  {_YELLOW}Add more models:{_RESET} {_DIM}{tips}{_RESET}" if _COLOR
                  else f"  Add more models: {tips}")
        print(_bar())

    def _draw_input_panel() -> str:
        """Read user input. Plain input() with no ANSI for Windows paste reliability."""
        print()
        print("─" * cols)
        try:
            user_input = input(" brynq> ")
        except (EOFError, KeyboardInterrupt):
            return ""
        print("─" * cols)
        return user_input.strip()

    def _print_message(sender: str, content: str, meta: str = "") -> None:
        print()
        header = f"  {sender}"
        if meta:
            header += f"  {_DIM}{meta}{_RESET}" if _COLOR else f"  {meta}"
        print(header)
        print(f"  {_bar('╌')}")
        for line in content.splitlines():
            print(f"  {line}")

    def _print_status(msg: str) -> None:
        print(f"\n  {_DIM}{msg}{_RESET}" if _COLOR else f"\n  {msg}")

    class _Spinner:
        """Live spinner that shows the user something is happening."""
        _FRAMES = [".", "..", "...", "....", "...."]

        def __init__(self, label: str):
            self.label = label
            self._stop = threading.Event()
            self._thread: threading.Thread | None = None

        def start(self):
            self._stop.clear()
            self._thread = threading.Thread(target=self._spin, daemon=True)
            self._thread.start()

        def _spin(self):
            i = 0
            while not self._stop.is_set():
                frame = self._FRAMES[i % len(self._FRAMES)]
                status = f"\r  {_DIM}{self.label}{frame}{_RESET}" if _COLOR else f"\r  {self.label}{frame}"
                sys.stdout.write(status)
                sys.stdout.flush()
                i += 1
                self._stop.wait(0.5)
            # Clear the spinner line
            sys.stdout.write(f"\r{' ' * (len(self.label) + 20)}\r")
            sys.stdout.flush()

        def stop(self):
            self._stop.set()
            if self._thread:
                self._thread.join(timeout=2)

    def _call_model_with_spinner(model: str, prompt: str) -> tuple[str, float]:
        """Call a model with a live spinner. Returns (result, elapsed_seconds)."""
        print(f"  {_model_label(model)} is typing...")
        label = f"Thinking: {model.split('/')[-1]} "
        spinner = _Spinner(label)
        spinner.start()
        start = time.time()
        try:
            result = _call_model(model, prompt)
        finally:
            spinner.stop()
        return result, time.time() - start

    chat_history: list[tuple[str, str, str, str]] = []
    _last_timer: list[tuple[str, float]] = []  # [(model, seconds), ...] from last exchange

    def _redraw() -> None:
        _draw_header()
        for role, sender, content, meta in chat_history:
            _print_message(sender, content, meta)
            if verbose and role == "assistant" and content:
                words = len(content.split())
                tokens = round(words * 1.3)
                print(f"  {_dim(f'{words} words | ~{tokens} tokens')}")

    _draw_header()

    # ── Compact startup banner ─────────────────────────────────────
    _n_models = len(active_models)
    _ollama_tag = f"{_GREEN_C}online{_RESET}" if ollama_ok else f"{_RED_C}offline{_RESET}"
    _ollama_tag_plain = "online" if ollama_ok else "offline"
    if _COLOR:
        print(f"  Brynq Omni-OS v1.0.45 (Singularity Build) | {_n_models} models active | Ollama: {_ollama_tag}")
    else:
        print(f"  Brynq Omni-OS v1.0.45 (Singularity Build) | {_n_models} models active | Ollama: {_ollama_tag_plain}")
    print(f"  Type /help for commands, /quit to exit")

    # ── Main loop ──────────────────────────────────────────────────
    _aliases = {}
    _default_model = None
    
    try:
        from runtime.exocortex.timeline_db import TimelineDB
        _timeline = TimelineDB()
    except Exception:
        _timeline = None

    while True:
        try:
            user_input = _draw_input_panel()
        except (EOFError, KeyboardInterrupt):
            print("\nExiting...")
            return 0
        except Exception as e:
            print(f"\nError reading input: {e}")
            return 1
            
        if not user_input:
            continue
            
        if _timeline and not user_input.startswith("/"):
            try:
                _timeline.log_event(int(time.time()), "user", user_input)
            except Exception:
                pass

        # Expand aliases
        if user_input.startswith("/") and not user_input.startswith("/alias"):
            _parts = user_input.split(maxsplit=1)
            _alias_name = _parts[0][1:].lower()
            if _alias_name in _aliases:
                user_input = _aliases[_alias_name] + (" " + _parts[1] if len(_parts) > 1 else "")

        # Handle commands
        if user_input.startswith("/"):
            parts = user_input.split(maxsplit=1)
            cmd = parts[0].lower()
            cmd_arg = parts[1].strip() if len(parts) > 1 else ""

            if cmd in ("/quit", "/exit", "/q"):
                sys.stdout.write("\033[2J\033[H")
                print(_dim("Goodbye from Brynq."))
                return 0
            elif cmd == "/add":
                if not cmd_arg:
                    print()
                    print("  Usage: /add <model>")
                    print()
                    print("  Examples:")
                    print("    /add claude          — Claude (needs Claude CLI installed)")
                    print("    /add gemini          — Gemini (needs Gemini CLI installed)")
                    print("    /add chatgpt         — ChatGPT (needs OPENAI_API_KEY env var)")
                    print("    /add ollama:llama3   — Llama 3 via Ollama")
                    print("    /add ollama:mistral  — Mistral via Ollama")
                    print("    /add ollama:qwen2.5  — Qwen 2.5 via Ollama")
                    if all_detected:
                        print(f"\n  Detected on your machine: {', '.join(all_detected)}")
                    print()
                elif cmd_arg in active_models:
                    print(f"  {cmd_arg} is already active")
                elif cmd_arg.startswith("ollama:") and ollama_ok:
                    # Auto-pull Ollama model if not already available
                    model_name = cmd_arg.split(":", 1)[1]
                    available = _ollama_list_models(config.ollama_url)
                    if model_name not in available:
                        print(f"  Pulling {model_name} via Ollama (this may take a minute)...")
                        try:
                            import subprocess as _sp_pull
                            _sp_pull.run(["ollama", "pull", model_name], timeout=300)
                            print(f"  {model_name} pulled successfully")
                        except Exception as e:
                            print(f"  Failed to pull {model_name}: {e}")
                            continue
                    active_models.append(cmd_arg)
                    if cmd_arg not in all_detected:
                        all_detected.append(cmd_arg)
                    _redraw()
                    print(f"  Added {cmd_arg}")
                elif cmd_arg in ("claude", "gemini", "chatgpt"):
                    active_models.append(cmd_arg)
                    if cmd_arg not in all_detected:
                        all_detected.append(cmd_arg)
                    _redraw()
                    print(f"  Added {cmd_arg}")
                elif cmd_arg not in all_detected:
                    # Try to add anyway — user might know what they're doing
                    active_models.append(cmd_arg)
                    all_detected.append(cmd_arg)
                    _redraw()
                    print(f"  Added {cmd_arg} (unverified — may not work)")
                else:
                    active_models.append(cmd_arg)
                    _redraw()
                continue
            elif cmd == "/remove":
                if cmd_arg in active_models and len(active_models) > 1:
                    active_models.remove(cmd_arg)
                _redraw()
                continue
            elif cmd == "/models":
                _redraw()
                print()
                for m in active_models:
                    code_tag = f" {_GREEN_C}[code]{_RESET}" if _COLOR and m in _CODE_MODELS else ""
                    print(f"  {_model_label(m)}{code_tag}")
                continue
            elif cmd == "/tools":
                installed = _get_installed_agents(config.data_dir)
                if not installed:
                    print("  No agents or tools installed yet. Browse at /marketplace.")
                else:
                    print()
                    print(f"  {_bold('Installed Tools & Agents')}")
                    for a in installed:
                        name = a.get("name", a.get("agent_id", "unknown"))
                        tools = a.get("tools", [])
                        print(f"  - {name}")
                        if tools:
                            for t in tools:
                                t_name = t.get("name", "unknown")
                                t_desc = t.get("description", "")
                                print(f"      {_dim(t_name)}: {_dim(t_desc)}")
                print()
                continue
            elif cmd == "/run":
                if not cmd_arg:
                    print("  Usage: /run <agent_name> <prompt>")
                    continue
                parts = cmd_arg.split(" ", 1)
                agent_name = parts[0]
                prompt = parts[1] if len(parts) > 1 else "Hello"
                
                print(f"  Running {agent_name}...")
                src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
                if src_path not in sys.path:
                    sys.path.insert(0, src_path)
                
                from runtime.executor.agent_bridge import AgentBridge
                bridge = AgentBridge()
                res = bridge.call_installed_agent(agent_name, prompt)
                
                if res.success:
                    print(f"\n  {_cyan(agent_name)}:\n  {res.output}\n")
                else:
                    print(f"\n  {_red('Error')}: {res.error}\n")
                continue
            elif cmd == "/status":
                print()
                print(f"  {_bold('Connected Models')}")
                print()
                for m in active_models:
                    if m == "claude":
                        ok = shutil.which("claude") is not None
                    elif m == "gemini":
                        ok = shutil.which("gemini") is not None
                    elif m.startswith("ollama:"):
                        ok = _ollama_is_running(config.ollama_url)
                    else:
                        ok = True
                    dot = _green("●") if ok else _red("●")
                    state = _green("online") if ok else _red("offline")
                    code_tag = f" {_GREEN_C}[code]{_RESET}" if _COLOR and m in _CODE_MODELS else ""
                    print(f"  {dot} {_model_label(m)}{code_tag}  {state}")
                print()
                # Ollama server
                ollama_up = _ollama_is_running(config.ollama_url)
                ollama_dot = _green("●") if ollama_up else _red("●")
                ollama_state = _green("running") if ollama_up else _red("not running")
                print(f"  {_bold('Services')}")
                print(f"  {ollama_dot} Ollama          {ollama_state}  ({config.ollama_url})")
                # Cloud brain
                cloud_up = _cloud_status.get("online")
                if cloud_up is True:
                    cloud_dot = _green("●")
                    cloud_state = _green("connected")
                elif cloud_up is False:
                    cloud_dot = _red("●")
                    cloud_state = _red("disconnected")
                else:
                    cloud_dot = _yellow("●")
                    cloud_state = _yellow("checking...")
                print(f"  {cloud_dot} Brynq Cloud     {cloud_state}")
                skill_count = _cloud_status.get("skill_count")
                if skill_count:
                    print(f"    Skills available: {skill_count:,}")
                print()
                continue
            elif cmd == "/clear":
                chat_history.clear()
                _redraw()
                continue
            elif cmd == "/history":
                print()
                recent = chat_history[-10:]
                if not recent:
                    print(f"  {_dim('No messages yet.')}" if _COLOR else "  No messages yet.")
                else:
                    for role, sender, content, meta in recent:
                        tag = _green(sender) if role == "user" else _cyan(sender) if _COLOR else sender
                        preview = content.replace("\n", " ")[:120]
                        print(f"  [{meta}] {tag}: {preview}")
                print()
                continue
            elif cmd == "/rag":
                from runtime.rag import add_file, list_files, remove_file
                if cmd_arg.startswith("add "):
                    filepath = cmd_arg[4:].strip().strip('"').strip("'")
                    try:
                        result = add_file(config.data_dir, filepath)
                        print(f"  Indexed: {result.get('chunks', 0)} chunks from {filepath}")
                    except Exception as e:
                        print(f"  {_red(f'Error: {e}')}")
                elif cmd_arg == "list":
                    files = list_files(config.data_dir)
                    if not files:
                        print("  No files indexed. Use /rag add <filepath>")
                    for f in files:
                        print(f"  {f['filepath']}  ({f.get('chunks', 0)} chunks)")
                elif cmd_arg.startswith("remove "):
                    filepath = cmd_arg[7:].strip().strip('"').strip("'")
                    if remove_file(config.data_dir, filepath):
                        print(f"  Removed: {filepath}")
                    else:
                        print(f"  Not found: {filepath}")
                elif cmd_arg == "off":
                    _rag_enabled = False
                    print("  RAG disabled for this session.")
                elif cmd_arg == "on":
                    _rag_enabled = True
                    print("  RAG enabled.")
                else:
                    print("  /rag add <filepath>  — index a file")
                    print("  /rag list            — show indexed files")
                    print("  /rag remove <file>   — remove from index")
                    print("  /rag on/off          — toggle RAG context")
                print()
                continue
            elif cmd == "/export":
                if not chat_history:
                    print("  Nothing to export — chat history is empty.")
                else:
                    from datetime import datetime as _dt
                    exports_dir = Path("exports")
                    exports_dir.mkdir(exist_ok=True)
                    stamp = _dt.now().strftime("%Y%m%d_%H%M%S")
                    out_path = exports_dir / f"chat_{stamp}.json"
                    entries = [
                        {"role": role, "sender": sender, "content": content, "meta": meta}
                        for role, sender, content, meta in chat_history
                    ]
                    out_path.write_text(json.dumps(entries, indent=2, ensure_ascii=False), encoding="utf-8")
                    print(f"  Exported {len(entries)} messages to {out_path}")
                print()
                continue
            elif cmd == "/config":
                print()
                print(f"  Cloud URL: {config.cloud_url}")
                print(f"  Ollama URL: {config.ollama_url}")
                print(f"  Runtime port: {config.local_port}")
                print(f"  Active models: {len(active_models)}")
                print(f"  Verbose mode: {'on' if verbose else 'off'}")
                print()
                continue
            elif cmd == "/timer":
                if not _last_timer:
                    print("  No timing data yet. Send a message first.")
                else:
                    print()
                    for model, seconds in _last_timer:
                        print(f"  {model}: {seconds:.1f}s")
                    print()
                continue
            elif cmd == "/usage":
                if not chat_history:
                    print("  No messages yet.")
                else:
                    role_tokens: dict[str, int] = {}
                    for role, _sender, content, _meta in chat_history:
                        est = len(content) // 4
                        role_tokens[role] = role_tokens.get(role, 0) + est
                    total = sum(role_tokens.values())
                    print()
                    print(f"  Estimated token usage: ~{total:,}")
                    for role, tokens in sorted(role_tokens.items()):
                        print(f"    {role}: ~{tokens:,}")
                    print()
                continue
            elif cmd == "/marketplace":
                stats = _marketplace_stats()
                total = stats.get("total_entries", 0)
                cats = stats.get("by_category", {})
                print()
                print(f"  {_bold('Brynq Marketplace')}: {total:,} tools")
                print()
                if cats:
                    for cat_name, count in sorted(cats.items(), key=lambda x: -x[1]):
                        bar = "#" * min(count // 20, 40)
                        print(f"    {cat_name:<14} {count:>5}  {_dim(bar)}" if _COLOR else f"    {cat_name:<14} {count:>5}  {bar}")
                print()
                print(f"  {_dim('Browse: brynq.ai/marketplace')}" if _COLOR else "  Browse: brynq.ai/marketplace")
                print(f"  {_dim('Install: /install <agent-id>')}" if _COLOR else "  Install: /install <agent-id>")
                print()
                continue
            elif cmd == "/tools-stats":
                log_file = config.data_dir / "tool_usage.jsonl"
                if not log_file.exists():
                    print("  No tool usage recorded yet.")
                else:
                    lines = log_file.read_text(encoding="utf-8").strip().splitlines()
                    entries = [json.loads(l) for l in lines if l.strip()]
                    installs = [e for e in entries if e["action"] == "auto_install"]
                    calls = [e for e in entries if e["action"] == "call"]
                    fails = [e for e in entries if e["action"] == "call_failed"]
                    # Count by tool
                    tool_counts: dict[str, int] = {}
                    for e in calls:
                        tool_counts[e["name"]] = tool_counts.get(e["name"], 0) + 1
                    print()
                    print(f"  {_bold('Tool Usage Stats')}")
                    print(f"  Auto-installs: {len(installs)}")
                    print(f"  Successful calls: {len(calls)}")
                    print(f"  Failed calls: {len(fails)}")
                    if tool_counts:
                        print(f"\n  {_bold('Most used tools:')}")
                        for name, count in sorted(tool_counts.items(), key=lambda x: -x[1])[:10]:
                            print(f"    {name}: {count} calls")
                    print()
                continue
            elif cmd == "/install":
                if not cmd_arg:
                    print("  Usage: /install <agent-id>")
                    print()
                    continue
                agent_id = cmd_arg.strip()
                cat = _load_catalog()
                if cat is None:
                    print(f"  {_red('Marketplace catalog not available.')}" if _COLOR else "  Marketplace catalog not available.")
                    print()
                    continue
                entry = cat.get_entry(agent_id)
                if entry is None:
                    print(f"  {_red('Agent not found:')}" f" {agent_id}" if _COLOR else f"  Agent not found: {agent_id}")
                    print()
                    continue
                # Save manifest to ~/.brynq/agents/{id}/manifest.json
                agent_dir = config.data_dir / "agents" / agent_id
                agent_dir.mkdir(parents=True, exist_ok=True)
                manifest = {
                    "agent_id": entry.id,
                    "name": entry.name,
                    "description": entry.description,
                    "category": entry.category,
                    "install_type": entry.install_type,
                    "source": entry.source,
                    "pricing": entry.pricing,
                    "installed_at": time.time(),
                    "status": "installed",
                }
                if entry.mcp_command:
                    manifest["mcp_command"] = entry.mcp_command
                if entry.mcp_url:
                    manifest["mcp_url"] = entry.mcp_url
                if entry.endpoint:
                    manifest["endpoint"] = entry.endpoint
                if entry.package_url:
                    manifest["package_url"] = entry.package_url
                (agent_dir / "manifest.json").write_text(
                    json.dumps(manifest, indent=2), encoding="utf-8",
                )
                print()
                print(f"  {_green('Installed:')} {entry.name} ({agent_id})" if _COLOR else f"  Installed: {entry.name} ({agent_id})")
                print(f"  {_dim(entry.description)}" if _COLOR else f"  {entry.description}")
                print(f"  Manifest saved to {agent_dir / 'manifest.json'}")
                print()
                continue
            elif cmd == "/uninstall":
                if not cmd_arg:
                    print("  Usage: /uninstall <agent-id>")
                    print()
                    continue
                agent_id = cmd_arg.strip()
                agent_dir = config.data_dir / "agents" / agent_id
                if not agent_dir.exists():
                    print(f"  {_red('Agent not installed:')}" f" {agent_id}" if _COLOR else f"  Agent not installed: {agent_id}")
                    print()
                    continue
                shutil.rmtree(agent_dir)
                print()
                print(f"  {_green('Uninstalled:')} {agent_id}" if _COLOR else f"  Uninstalled: {agent_id}")
                print(f"  Removed {agent_dir}")
                print()
                continue
            elif cmd == "/installed":
                agents_dir = config.data_dir / "agents"
                manifests: list[dict] = []
                if agents_dir.exists():
                    for mf in sorted(agents_dir.glob("*/manifest.json")):
                        try:
                            manifests.append(json.loads(mf.read_text(encoding="utf-8")))
                        except (json.JSONDecodeError, OSError):
                            continue
                print()
                if not manifests:
                    print("  No agents installed yet. Try /marketplace to find some.")
                else:
                    print(f"  {'Name':<25} {'Type':<12} {'Category':<15} {'Installed'}")
                    print(f"  {'-'*25} {'-'*12} {'-'*15} {'-'*19}")
                    for m in manifests:
                        name = m.get("name", "unknown")[:25]
                        itype = m.get("install_type", "unknown")[:12]
                        cat = m.get("category", "unknown")[:15]
                        ts = m.get("installed_at")
                        if ts:
                            from datetime import datetime, timezone
                            date_str = datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%d %H:%M")
                        else:
                            date_str = "unknown"
                        print(f"  {name:<25} {itype:<12} {cat:<15} {date_str}")
                    print(f"\n  {len(manifests)} agent(s) installed.")
                print()
                continue
            elif cmd == "/volunteer":
                from runtime.network.volunteer_compute import VolunteerCompute
                vc = VolunteerCompute(config.data_dir)
                if cmd_arg == "on":
                    vc.opt_in()
                    vc.start()
                    print("  Volunteer compute enabled. Benchmarks will run when idle.")
                elif cmd_arg == "off":
                    vc.opt_out()
                    print("  Volunteer compute disabled.")
                elif cmd_arg == "status":
                    stats = vc.get_stats()
                    print(f"  Enabled: {stats['enabled']}")
                    print(f"  Cycles: {stats['total_cycles']}")
                    print(f"  Benchmarks: {stats['total_benchmarks']}")
                else:
                    print("  Usage: /volunteer on|off|status")
                continue
            elif cmd == "/diagnose":
                print("\n  Running diagnostics...")
                checks = []
                checks.append(("Ollama", "running" if _ollama_is_running(config.ollama_url) else "offline"))
                checks.append(("Cloud API", "connected" if _cloud_status.get("online") else "unreachable"))
                checks.append(("Node.js", "found" if has_node else "missing"))
                checks.append(("Claude CLI", "found" if has_claude else "missing"))
                checks.append(("Gemini CLI", "found" if has_gemini else "missing"))
                for name, status in checks:
                    color = _green if status in ["running", "connected", "found"] else _red
                    print(f"  {name:<15} {color(status)}")
                print()
                continue
            elif cmd == "/benchmark":
                if not active_models:
                    print("  No active models to benchmark.")
                    continue
                print("\n  Benchmarking active models...")
                b_prompt = "Say hello in exactly one sentence."
                for m in active_models:
                    start_t = time.time()
                    try:
                        _call_model(m, b_prompt)
                        elapsed = time.time() - start_t
                        print(f"  {m:<15} {elapsed:.2f}s")
                    except Exception as e:
                        print(f"  {m:<15} error: {e}")
                print()
                continue
            elif cmd == "/compare":
                if not cmd_arg:
                    print("  Usage: /compare <prompt>")
                    continue
                print(f"\n  Comparing {len(active_models)} models for: {cmd_arg}\n")
                for m in active_models:
                    start_t = time.time()
                    res = _call_model(m, cmd_arg)
                    elapsed = time.time() - start_t
                    print(f"  === {m} ({elapsed:.1f}s) ===")
                    print(f"  {res}\n")
                continue
            elif cmd == "/sessions":
                if not _learning_available:
                    print("  Analytics not available.")
                    continue
                sessions = _session_analytics.get_recent_sessions(limit=5)
                print("\n  Recent Sessions:")
                for s in sessions:
                    print(f"  ID: {s['session_id']} | Models: {s['models']} | Prompts: {s['prompt_count']}")
                print()
                continue
            elif cmd == "/alias":
                if not cmd_arg:
                    print("\n  Active Aliases:")
                    if not _aliases:
                        print("    None")
                    else:
                        for k, v in _aliases.items():
                            print(f"    /{k} -> {v}")
                    print("  Usage: /alias <name> <command>")
                    print()
                    continue
                alias_parts = cmd_arg.split(" ", 1)
                if len(alias_parts) == 2:
                    _aliases[alias_parts[0]] = alias_parts[1]
                    print(f"  Alias saved: /{alias_parts[0]} -> {alias_parts[1]}")
                else:
                    print("  Usage: /alias <name> <command>")
                continue
            elif cmd == "/summary":
                if not chat_history:
                    print("  No messages to summarize.")
                    continue
                recent = "\n".join(f"{s}: {c}" for _, s, c, _ in chat_history[-10:])
                print("\n  Generating summary...")
                model = active_models[0] if active_models else "claude"
                res = _call_model(model, f"Summarize these recent messages concisely:\n\n{recent}")
                print(f"\n  Summary: {res}\n")
                continue
            elif cmd == "/replay":
                if not chat_history:
                    print("  No messages to replay.")
                    continue
                try:
                    idx = int(cmd_arg) if cmd_arg else -1
                    if idx > 0 and idx <= len(chat_history):
                        idx = idx - 1
                    elif idx < 0 and abs(idx) <= len(chat_history):
                        pass
                    else:
                        print(f"  Invalid index: {cmd_arg}")
                        continue
                    msg = chat_history[idx][2]
                    print(f"\n  Replaying message: {msg[:50]}...")
                    user_input = msg
                    # Don't continue here, we want it to be processed outside the command block!
                except ValueError:
                    print("  Usage: /replay <index or negative offset>")
                    continue
            elif cmd == "/save":
                if not chat_history:
                    print("  No messages to save.")
                    continue
                last_asst = next((c for r, _, c, _ in reversed(chat_history) if r == "assistant"), None)
                if not last_asst:
                    print("  No assistant message to save.")
                    continue
                fname = cmd_arg or f"brynq_save_{int(time.time())}.txt"
                Path(fname).write_text(last_asst, encoding="utf-8")
                print(f"  Saved last response to {fname}")
                continue
            elif cmd == "/tokens":
                if not chat_history:
                    print("  No tokens used yet.")
                    continue
                total = sum(len(c)//4 for _, _, c, _ in chat_history)
                print(f"  Session estimated tokens: ~{total}")
                continue
            elif cmd == "/model":
                if not cmd_arg:
                    print(f"  Current default model: {_default_model or active_models[0] if active_models else 'None'}")
                    continue
                if cmd_arg in active_models:
                    _default_model = cmd_arg
                    active_models.remove(cmd_arg)
                    active_models.insert(0, cmd_arg)
                    print(f"  Set default model to {cmd_arg}")
                else:
                    print(f"  Model {cmd_arg} not active. Active: {', '.join(active_models)}")
                continue
            elif cmd == "/trending":
                cat = _get_catalog_and_stats()[0]
                if not cat:
                    print("  Marketplace catalog unavailable.")
                    continue
                try:
                    entries = sorted(cat.entries, key=lambda e: getattr(e, 'downloads', 0) or 0, reverse=True)[:5]
                    print("\n  Trending Agents:")
                    for i, e in enumerate(entries, 1):
                        print(f"  {i}. {e.name} - {e.category} ({getattr(e, 'downloads', 0)} dls)")
                    print()
                except Exception:
                    print("  Could not fetch trending agents.")
                continue
            elif cmd == "/categories":
                stats = _get_catalog_and_stats()[1]
                cats = stats.get("by_category", {})
                if not cats:
                    print("  No categories found.")
                    continue
                print("\n  Marketplace Categories:")
                for c, count in sorted(cats.items(), key=lambda x: -x[1]):
                    print(f"  - {c}: {count}")
                print()
                continue
            elif cmd == "/info":
                if not cmd_arg:
                    print("  Usage: /info <agent-id>")
                    continue
                cat = _get_catalog_and_stats()[0]
                if not cat:
                    print("  Marketplace catalog unavailable.")
                    continue
                e = cat.get_entry(cmd_arg)
                if not e:
                    print(f"  Agent '{cmd_arg}' not found.")
                    continue
                print(f"\n  Name: {e.name}")
                print(f"  ID: {e.id}")
                print(f"  Category: {e.category}")
                print(f"  Description: {e.description}")
                print(f"  Type: {e.install_type}")
                print(f"  Rating: {getattr(e, 'rating', 'N/A')}")
                print()
                continue
            elif cmd == "/browser":
                if not cmd_arg:
                    print("  Usage: /browser <url>")
                    continue
                try:
                    from runtime.computer_use.browser import BrowserAutomation
                    browser = BrowserAutomation()
                    res = browser.navigate(cmd_arg)
                    print(f"  [Browser] {res}")
                except Exception as e:
                    print(f"  [Browser] Error: {e}")
                continue
            elif cmd == "/desktop":
                if not cmd_arg:
                    print("  Usage: /desktop <action>")
                    continue
                try:
                    from runtime.computer_use.desktop import DesktopAutomation
                    desktop = DesktopAutomation()
                    if cmd_arg.startswith("type "):
                        res = desktop.type_text(cmd_arg[5:])
                        print(f"  [Desktop] {res}")
                    elif cmd_arg.startswith("click "):
                        coords = cmd_arg[6:].split()
                        if len(coords) == 2:
                            res = desktop.click(int(coords[0]), int(coords[1]))
                            print(f"  [Desktop] {res}")
                    elif cmd_arg == "screenshot":
                        res = desktop.take_screenshot()
                        print(f"  [Desktop] {res}")
                    else:
                        print("  Usage: /desktop type <text> | click <x> <y> | screenshot")
                except Exception as e:
                    print(f"  [Desktop] Error: {e}")
                continue
            elif cmd == "/schedule":
                parts = cmd_arg.split(" ", 5) if cmd_arg else []
                if len(parts) < 6:
                    print("  Usage: /schedule <cron> <task>")
                    print("  Example: /schedule * * * * * summarize my emails")
                    continue
                cron_expr = " ".join(parts[:5])
                task_desc = parts[5]
                try:
                    from runtime.daemons.cron import AICronManager
                    manager = AICronManager()
                    manager.schedule_task(cron_expr, lambda t=task_desc: print(f"\n  [Cron] Executing: {t}"))
                    manager.start_scheduler()
                    print(f"  [Cron] Scheduled: '{task_desc}' with schedule '{cron_expr}'")
                except Exception as e:
                    print(f"  [Cron] Error: {e}")
                continue
            elif cmd == "/build-tool":
                if not cmd_arg:
                    print("  Usage: /build-tool <description>")
                    continue
                try:
                    from runtime.meta.tool_builder import ToolBuilder
                    from runtime.meta.sandbox import ExecutionSandbox
                    builder = ToolBuilder()
                    sandbox = ExecutionSandbox()
                    code = builder.generate_tool(cmd_arg)
                    print("  [Meta] Generated Code:")
                    print("  " + "\n  ".join(code.splitlines()))
                    try:
                        sandbox.run_safely(code)
                        print("  [Meta] Code is safe and valid.")
                    except Exception as e:
                        print(f"  [Meta] Safety Error: {e}")
                except Exception as e:
                    print(f"  [Meta] Error: {e}")
                continue
            elif cmd == "/grid":
                if not cmd_arg:
                    print("  Usage: /grid join | status")
                    continue
                try:
                    from runtime.grid.node import GridNode
                    node = GridNode()
                    if cmd_arg == "join":
                        node.advertise_presence()
                        print("  [Grid] Joined the local peer-to-peer compute grid.")
                    elif cmd_arg == "status":
                        peers = node.discover_peers()
                        print(f"  [Grid] Active peers: {len(peers)}")
                        for p in peers:
                            print(f"    - {p['node_id']} ({p['status']})")
                except Exception as e:
                    print(f"  [Grid] Error: {e}")
                continue
            elif cmd == "/simulate":
                if not cmd_arg:
                    print("  Usage: /simulate <task>")
                    continue
                try:
                    from runtime.simulation.sandbox import VMSandbox
                    from runtime.simulation.blast_radius import BlastRadiusAnalyzer
                    print(f"  [Sim] Spinning up ephemeral sandbox for: {cmd_arg[:30]}...")
                    sandbox = VMSandbox()
                    sandbox.mount_workspace("/mock/src", "/mock/dest")
                    out = sandbox.execute_isolated(cmd_arg)
                    print(f"  [Sim] Execution complete. Analyzing blast radius...")
                    analyzer = BlastRadiusAnalyzer()
                    impact = analyzer.analyze_diff(out)
                    print(f"  [Sim] Severity: {impact['severity']}/10")
                    print(f"  [Sim] Files affected: {', '.join(impact['affected_files'])}")
                except Exception as e:
                    print(f"  [Sim] Error: {e}")
                continue
            elif cmd == "/wallet":
                try:
                    from runtime.billing.wallet import LocalWallet
                    wallet = LocalWallet()
                    bal = wallet.get_balance()
                    print(f"  [Wallet] Current balance: {bal:.4f} credits")
                except Exception as e:
                    print(f"  [Wallet] Error: {e}")
                continue
            elif cmd == "/policy":
                if cmd_arg == "sync":
                    try:
                        from runtime.enterprise.policy import PolicyEngine
                        engine = PolicyEngine()
                        # Mock remote rules fetch
                        engine.load_remote_rules({"cloud_upload": False, "allow_shell": True})
                        print("  [Policy] Enterprise policies synced successfully.")
                    except Exception as e:
                        print(f"  [Policy] Error: {e}")
                else:
                    print("  Usage: /policy sync")
                continue
            elif cmd == "/listen":
                if not cmd_arg or cmd_arg not in ["start", "stop"]:
                    print("  Usage: /listen start | stop")
                    continue
                try:
                    from runtime.multimodal.audio import AudioStreamer
                    if not hasattr(sys, "_audio_streamer"):
                        sys._audio_streamer = AudioStreamer()
                    if cmd_arg == "start":
                        sys._audio_streamer.start_listening()
                        print("  [Audio] Listening for voice commands... (Mocked)")
                    else:
                        sys._audio_streamer.stop_listening()
                        print("  [Audio] Stopped listening.")
                except Exception as e:
                    print(f"  [Audio] Error: {e}")
                continue
            elif cmd == "/predict":
                print("  [Proactive] Scanning environment...")
                try:
                    from runtime.proactive.watcher import ContextWatcher
                    from runtime.proactive.intent import IntentPredictor
                    watcher = ContextWatcher()
                    predictor = IntentPredictor()
                    
                    git_state = watcher.monitor_git_state()
                    context = f"Git: {git_state['status']} | Log: {git_state['last_commit']}"
                    prediction = predictor.predict_need(context)
                    
                    if prediction["confidence"] > 0:
                        print(f"  [Proactive] Intent detected: {prediction['intent']} (Conf: {prediction['confidence']}%)")
                        print("  [Proactive] Would you like me to spawn a swarm to handle this? (Type /swarm auto to execute)")
                    else:
                        print("  [Proactive] No immediate actions predicted.")
                except Exception as e:
                    print(f"  [Proactive] Error: {e}")
                continue
            elif cmd == "/hyperswarm":
                if not cmd_arg:
                    print("  Usage: /hyperswarm <massive abstract goal>")
                    continue
                try:
                    from runtime.hyperswarm.meta_orchestrator import HyperSwarmOrchestrator
                    from runtime.hyperswarm.fractal_state import FractalStateManager
                    
                    orchestrator = HyperSwarmOrchestrator()
                    state_mgr = FractalStateManager()
                    
                    print(f"  [HyperSwarm] Decomposing massive goal: {cmd_arg[:50]}...")
                    subgoals = orchestrator.decompose_goal(cmd_arg)
                    print(f"  [HyperSwarm] Found {len(subgoals)} core objectives.")
                    
                    managers = orchestrator.spawn_managers(subgoals)
                    print(f"  [HyperSwarm] Spawned {len(managers)} Manager Agents.")
                    
                    # Track initial state
                    tree = {}
                    for mgr in managers:
                        tree[mgr["id"]] = {"goal": mgr["goal"], "status": "planning_subtasks"}
                    state_mgr.track_tree("root", tree)
                    
                    print("  [HyperSwarm] Hierarchy established. Type /hyperstatus to monitor the tree.")
                except Exception as e:
                    print(f"  [HyperSwarm] Error: {e}")
                continue
            elif cmd == "/hyperstatus":
                try:
                    from runtime.hyperswarm.fractal_state import FractalStateManager
                    state_mgr = FractalStateManager()
                    tree = state_mgr.get_tree_status("root")
                    if not tree:
                        print("  [HyperSwarm] No active hyper-swarm.")
                    else:
                        print("  [HyperSwarm] Active Fractal Tree:")
                        print("  " + json.dumps(tree, indent=2).replace("\n", "\n  "))
                except Exception as e:
                    print(f"  [HyperSwarm] Error: {e}")
                continue
            elif cmd == "/hivemind":
                if not cmd_arg or not cmd_arg.startswith("lookup "):
                    print("  Usage: /hivemind lookup <error>")
                    continue
                try:
                    from runtime.hivemind.dht_node import HivemindNode
                    import hashlib
                    node = HivemindNode()
                    err_hash = hashlib.sha256(cmd_arg[7:].encode()).hexdigest()[:8]
                    sol = node.lookup_solution(err_hash)
                    if sol:
                        print(f"  [Hivemind] Found global fix for '{cmd_arg[7:]}':\n  {sol}")
                    else:
                        print(f"  [Hivemind] No global fix found for hash {err_hash}. You are on your own.")
                except Exception as e:
                    print(f"  [Hivemind] Error: {e}")
                continue
            elif cmd == "/market":
                if cmd_arg == "status":
                    try:
                        from runtime.finance.market import ComputeMarketplace
                        market = ComputeMarketplace()
                        # Mock listing
                        market.list_idle_capacity({"gpu": "RTX 4090", "ram": "64GB"})
                        print("  [Market] Your idle compute is listed on the Global Grid.")
                    except Exception as e:
                        print(f"  [Market] Error: {e}")
                else:
                    print("  Usage: /market status")
                continue
            elif cmd == "/provision":
                if not cmd_arg:
                    print("  Usage: /provision <architecture>")
                    continue
                try:
                    from runtime.finance.auto_buyer import CloudProvisioner
                    buyer = CloudProvisioner()
                    architecture = {"instances": [{"type": cmd_arg, "hours": 730, "count": 1}]}
                    cost = buyer.estimate_cost(architecture)
                    print(f"  [Finance] Estimated cost for '{cmd_arg}': ${cost:.2f}/mo")
                    res = buyer.provision_mock_aws_spot(cmd_arg, cost)
                    print(f"  [Finance] {res}")
                except Exception as e:
                    print(f"  [Finance] Error: {e}")
                continue
            elif cmd == "/deploy":
                if not cmd_arg:
                    print("  Usage: /deploy <project_dir>")
                    continue
                try:
                    from runtime.devops.terraform_gen import TerraformGenerator
                    from runtime.devops.deployer import AutoDeployer
                    gen = TerraformGenerator()
                    tf_code = gen.generate_nginx_conf(f"brynq-{int(time.time())}.local")
                    print("  [DevOps] Generated infrastructure config...")
                    deployer = AutoDeployer()
                    out = deployer.execute_terraform(cmd_arg)
                    print(f"  [DevOps] {out}")
                    status = deployer.check_uptime("http://localhost")
                    print(f"  [DevOps] {status}")
                except Exception as e:
                    print(f"  [DevOps] Error: {e}")
                continue
            elif cmd == "/iot":
                parts = cmd_arg.split(" ") if cmd_arg else []
                if len(parts) != 3 or parts[0] != "trigger":
                    print("  Usage: /iot trigger <device> <state>")
                    continue
                try:
                    from runtime.iot.home_assistant import HomeAssistantAdapter
                    ha = HomeAssistantAdapter("http://mock-ha.local")
                    res = ha.toggle_device(parts[1], parts[2])
                    print(f"  [IoT] {res['status']} — {res['device_id']} → {res['state']}")
                except Exception as e:
                    print(f"  [IoT] Error: {e}")
                continue
            elif cmd == "/empathy":
                try:
                    from runtime.cognition.empathy import UserStateDetector
                    detector = UserStateDetector()
                    history_text = " ".join([c for _, _, c, _ in chat_history]) if chat_history else ""
                    state = detector.analyze_sentiment(history_text)
                    print(f"  [Empathy] Current Detected State: {state.name}")
                except Exception as e:
                    print(f"  [Empathy] Error: {e}")
                continue
            elif cmd == "/translate":
                parts = cmd_arg.rsplit(" ", 1) if cmd_arg else []
                if len(parts) != 2:
                    print("  Usage: /translate <source_code_string> <target_lang>")
                    continue
                try:
                    from runtime.compilation.translator import CodeTranslator
                    from runtime.compilation.validator import ParityValidator
                    translator = CodeTranslator()
                    validator = ParityValidator()
                    code = parts[0]
                    target = parts[1]
                    translated = translator.translate_ast(code, "unknown", target)
                    print(f"  [Compiler] Translated to {target}: {translated}")
                    validator.run_cross_tests("mock_bin1", "mock_bin2", [])
                    print("  [Compiler] Parity tests passed.")
                except Exception as e:
                    print(f"  [Compiler] Error: {e}")
                continue
            elif cmd == "/recall":
                if not cmd_arg:
                    print("  Usage: /recall <query>")
                    continue
                try:
                    from runtime.exocortex.context_builder import ExoContext
                    from runtime.exocortex.timeline_db import TimelineDB
                    exo = ExoContext(TimelineDB())
                    ctx = exo.build_from_query(cmd_arg)
                    print(f"  [Exocortex] Recalled context:\n  {ctx}")
                except Exception as e:
                    print(f"  [Exocortex] Error: {e}")
                continue
            elif cmd == "/scan":
                print("  [Immune] Scanning local environment...")
                try:
                    from runtime.security.immune_system import ImmuneDaemon
                    daemon = ImmuneDaemon()
                    lock_content = ""
                    for lock_name in ("requirements.txt", "poetry.lock", "package-lock.json", "Pipfile.lock"):
                        lock_path = Path.cwd() / lock_name
                        if lock_path.exists():
                            lock_content = lock_path.read_text("utf-8", errors="ignore")
                            break
                    deps = daemon.scan_dependencies(lock_content)
                    ports = daemon.fuzz_local_ports()
                    print(f"  [Immune] Dependency scan: {deps}")
                    print(f"  [Immune] Port fuzzing: {ports}")
                except Exception as e:
                    print(f"  [Immune] Error: {e}")
                continue
            elif cmd == "/forge":
                if not cmd_arg:
                    print("  Usage: /forge <benchmark_task>")
                    continue
                try:
                    from runtime.evolution.forge import AgentForge
                    from runtime.evolution.fitness import FitnessEvaluator
                    forge = AgentForge()
                    evaluator = FitnessEvaluator()
                    pop = forge.spawn_generation("You are a helpful assistant.", size=5)
                    print(f"  [Forge] Spawned {len(pop)} mutated agents.")
                    scores = evaluator.evaluate_population(pop, cmd_arg)
                    winners = evaluator.select_winners(scores, top_k=2)
                    print("  [Forge] Top Agents:")
                    for w in winners:
                        print(f"    - Score {w['score']}: {w['prompt'][:50]}...")
                except Exception as e:
                    print(f"  [Forge] Error: {e}")
                continue
            elif cmd == "/dream":
                if cmd_arg == "start":
                    try:
                        from runtime.singularity.dream_state import DreamLoop
                        loop = DreamLoop()
                        hyp = loop.generate_hypothesis()
                        print(f"  [Singularity] Generated hypothesis: {hyp}")
                        proof = loop.formulate_proof(hyp)
                        print(f"  [Singularity] Formulated proof: {proof}")
                        from runtime.singularity.epiphany import EpiphanyLogger
                        logger = EpiphanyLogger()
                        paper = logger.publish_whitepaper(hyp, proof)
                        print("  [Singularity] Whitepaper published to /epiphanies")
                    except Exception as e:
                        print(f"  [Singularity] Error: {e}")
                else:
                    print("  Usage: /dream start")
                continue
            elif cmd == "/epiphanies":
                print("  [Singularity] Displaying latest epiphanies (Mocked)...")
                try:
                    from runtime.singularity.epiphany import EpiphanyLogger
                    logger = EpiphanyLogger()
                    print("  " + logger.publish_whitepaper("P=NP", "Trivial by inspection.").replace("\n", "\n  "))
                except Exception as e:
                    print(f"  [Singularity] Error: {e}")
                continue
            elif cmd == "/cache":
                try:
                    from runtime.memory.neural_cache import SemanticCache
                    import numpy as np
                    if not hasattr(sys, "_neural_cache"):
                        sys._neural_cache = SemanticCache()
                    # Just display a mock status for now since actual cache hit requires an embedding model
                    print(f"  [NeuralCache] Active. Simulated cache hits: 0, misses: 0")
                except Exception as e:
                    print(f"  [NeuralCache] Error: {e}")
                continue
            elif cmd == "/scrub":
                if not cmd_arg:
                    print("  Usage: /scrub <text_with_pii>")
                    continue
                try:
                    from runtime.security.pii_scrubber import PIIScrubber
                    scrubber = PIIScrubber()
                    scrubbed, mapping = scrubber.anonymize(cmd_arg)
                    print(f"  [Scrubber] Anonymized: {scrubbed}")
                    print(f"  [Scrubber] Mapping: {mapping}")
                except Exception as e:
                    print(f"  [Scrubber] Error: {e}")
                continue
            elif cmd == "/vote":
                if not cmd_arg:
                    print("  Usage: /vote <task_description>")
                    continue
                try:
                    from runtime.executor.consensus import SwarmConsensus
                    consensus = SwarmConsensus()
                    # Mock generating some proposals
                    proposals = [
                        {"id": "agent_1", "content": "Deploy to AWS using Terraform", "score": 8.0},
                        {"id": "agent_2", "content": "Deploy to GCP using Pulumi", "score": 7.5},
                        {"id": "agent_3", "content": "Deploy to Azure using ARM", "score": 9.2}
                    ]
                    winner = consensus.conduct_vote(proposals, {"score": 1.0})
                    print(f"  [Consensus] The swarm has voted.")
                    print(f"  [Consensus] Winning Proposal: {winner['id']} -> {winner['content']}")
                except Exception as e:
                    print(f"  [Consensus] Error: {e}")
                continue
            elif cmd == "/api":
                if cmd_arg == "start":
                    try:
                        import threading
                        import uvicorn
                        from runtime.api.omniverse import app as omni_app
                        def run_server():
                            uvicorn.run(omni_app, host="127.0.0.1", port=8081, log_level="warning")
                        t = threading.Thread(target=run_server, daemon=True)
                        t.start()
                        print("  [Omniverse] Universal API started on http://127.0.0.1:8081")
                    except Exception as e:
                        print(f"  [Omniverse] Error starting API: {e}")
                else:
                    print("  Usage: /api start")
                continue
            elif cmd == "/review":
                parts = cmd_arg.split(" ") if cmd_arg else []
                if len(parts) != 2:
                    print("  Usage: /review <repo> <pr_num>")
                    continue
                try:
                    from runtime.integrations.github_ci import GitHubCIHook
                    hook = GitHubCIHook(token="dummy_token_for_cli")
                    repo = parts[0]
                    pr_num = int(parts[1])
                    print(f"  [GitHub CI] Fetching diff for {repo} PR #{pr_num}...")
                    try:
                        diff = hook.fetch_pr_diff(repo, pr_num)
                        print(f"  [GitHub CI] Diff fetched ({len(diff)} chars).")
                    except Exception as ex:
                        print(f"  [GitHub CI] Could not fetch real diff (expected if no auth): {ex}")
                    print(f"  [GitHub CI] Mocking autonomous review posting...")
                except Exception as e:
                    print(f"  [GitHub CI] Error: {e}")
                continue
            elif cmd == "/sync":
                print("  [CloudSync] Initializing local database sync...")
                try:
                    from runtime.cloud.sync_engine import CloudSync
                    db_path = str(config.data_dir / "exocortex.db") if (config.data_dir / "exocortex.db").exists() else "timeline.db"
                    syncer = CloudSync()
                    success = syncer.sync_exocortex(db_path)
                    if success:
                        print("  [CloudSync] Successfully synced exocortex to Brynq Cloud.")
                    else:
                        print("  [CloudSync] Sync failed.")
                except Exception as e:
                    print(f"  [CloudSync] Error: {e}")
                continue
            elif cmd == "/build-dataset":
                try:
                    from runtime.data_moat.dataset_builder import DatasetBuilder
                    # We just use the current chat history for the demo
                    events = [{"role": m[0], "content": m[2]} for m in chat_history if m[0] in ("user", "assistant")]
                    builder = DatasetBuilder()
                    jsonl = builder.format_for_finetuning(events)
                    out_file = cmd_arg if cmd_arg else "finetune.jsonl"
                    Path(out_file).write_text(jsonl, encoding="utf-8")
                    print(f"  [Data Moat] Built {out_file} ({len(jsonl.splitlines())} training pairs).")
                except Exception as e:
                    print(f"  [Data Moat] Error: {e}")
                continue
            elif cmd == "/global-ask":
                if not cmd_arg:
                    print("  Usage: /global-ask <query>")
                    continue
                try:
                    from runtime.data_moat.global_rag import GlobalKnowledgeBase
                    rag = GlobalKnowledgeBase()
                    results = rag.query_global_mind(cmd_arg)
                    print(f"  [Global RAG] Found {len(results)} matches from the Global Hivemind:")
                    for r in results:
                        print(f"    - {r.get('text', '')[:100]} (score: {r.get('score', 0)})")
                except Exception as e:
                    print(f"  [Global RAG] Error: {e}")
                continue
            elif cmd == "/login":
                if not cmd_arg:
                    print("  Usage: /login <api_key>")
                    continue
                try:
                    from runtime.auth.identity import IdentityManager
                    if not hasattr(sys, "_brynq_identity"):
                        sys._brynq_identity = IdentityManager()
                    res = sys._brynq_identity.login(cmd_arg)
                    if res:
                        print("  [Auth] Successfully logged into Brynq Cloud.")
                    else:
                        print("  [Auth] Login failed.")
                except Exception as e:
                    print(f"  [Auth] Error: {e}")
                continue
            elif cmd == "/whoami":
                try:
                    if hasattr(sys, "_brynq_identity") and sys._brynq_identity.token:
                        print(f"  [Auth] Logged in. Token: {sys._brynq_identity.token[:15]}...")
                    else:
                        print("  [Auth] Not logged in.")
                except Exception as e:
                    print(f"  [Auth] Error: {e}")
                continue
            elif cmd == "/use-native":
                try:
                    from runtime.models.brynq_native import BrynqNativeAdapter
                    if "brynq-coder-v1" not in active_models:
                        active_models.insert(0, "brynq-coder-v1")
                    _default_model = "brynq-coder-v1"
                    # Mock adapter instance stored globally for demo purposes
                    sys._brynq_native = BrynqNativeAdapter()
                    print("  [Models] Switched default model to proprietary Brynq-Coder-v1")
                except Exception as e:
                    print(f"  [Models] Error: {e}")
                continue
            elif cmd == "/room":
                if not cmd_arg:
                    print("  Usage: /room create <name> | join <code> | status | leave | close | invite <user> | kick <user>")
                    continue
                parts = cmd_arg.split(" ", 1)
                action = parts[0]
                arg = parts[1] if len(parts) > 1 else ""
                try:
                    from runtime.rooms.room_manager import room_manager
                    import asyncio
                    if action == "create":
                        room_name = arg or "Untitled Room"
                        room = asyncio.get_event_loop().run_until_complete(room_manager.create_room(room_name))
                        print(f"  [Room] Created: {room.get('room_code', 'N/A')}")
                        print(f"  [Room] Share this code. Waiting for peers...")
                        print(f"  [Room] Your relay is live on port 8081.")
                    elif action == "join":
                        if not arg:
                            print("  Usage: /room join <code>")
                            continue
                        room = asyncio.get_event_loop().run_until_complete(room_manager.join_room(arg))
                        print(f"  [Room] Connected to \"{room.get('name', 'Unknown')}\" (hosted by {room.get('host_id', 'unknown')})")
                        print(f"  [Room] Your models are shared with the swarm.")
                    elif action == "status":
                        if not room_manager.active_room:
                            print("  [Room] Not in a room. Use /room create or /room join <code>")
                        else:
                            r = room_manager.active_room
                            role = "HOST" if room_manager.is_host else "MEMBER"
                            print(f"  [Room] \"{r.get('name', 'Unknown')}\" ({r.get('room_code', 'N/A')}) [{role}]")
                            members = r.get("members", [])
                            print(f"  Members: {len(members)} connected")
                            for m in members:
                                models = ", ".join(m.get("models", [])) or "none"
                                gpu = m.get("compute", {}).get("gpu", "unknown")
                                print(f"    {m.get('username', '?')} — {models} | {gpu}")
                    elif action == "leave":
                        asyncio.get_event_loop().run_until_complete(room_manager.leave_room())
                        print("  [Room] Left the room.")
                    elif action == "close":
                        asyncio.get_event_loop().run_until_complete(room_manager.close_room())
                        print("  [Room] Room closed.")
                    elif action == "invite":
                        print(f"  [Room] Invite sent to {arg}. (Cloud directory lookup — coming soon)")
                    elif action == "kick":
                        print(f"  [Room] Kicked {arg}. (Admin enforcement — coming soon)")
                    else:
                        print(f"  [Room] Unknown action: {action}")
                except Exception as e:
                    print(f"  [Room] Error: {e}")
                continue
            elif cmd == "/chat":
                if not cmd_arg:
                    print("  Usage: /chat <message>")
                    continue
                try:
                    from runtime.rooms.room_manager import room_manager
                    if not room_manager.active_room:
                        print("  [Chat] Not in a room. Use /room create or /room join first.")
                    else:
                        room_manager.send_message(cmd_arg)
                        from runtime.rooms.profile import local_profile
                        name = local_profile.get().get("username", "You")
                        print(f"  [{name}] {cmd_arg}")
                except Exception as e:
                    print(f"  [Chat] Error: {e}")
                continue
            elif cmd == "/profile":
                if not cmd_arg:
                    print("  Usage: /profile set <field> <value> | show <field> true|false | view <username>")
                    continue
                parts = cmd_arg.split(" ", 2)
                action = parts[0]
                try:
                    from runtime.rooms.profile import local_profile
                    if action == "set" and len(parts) >= 3:
                        field = parts[1]
                        value = parts[2]
                        local_profile.update(**{field: value})
                        print(f"  [Profile] Set {field} = {value}")
                    elif action == "show" and len(parts) >= 3:
                        field = parts[1]
                        visible = parts[2].lower() == "true"
                        local_profile.update(visibility={field: visible})
                        print(f"  [Profile] {field} visibility = {visible}")
                    elif action == "view":
                        if len(parts) >= 2:
                            print(f"  [Profile] Lookup for '{parts[1]}' — cloud directory coming soon.")
                        else:
                            prof = local_profile.get()
                            print(f"  [Profile] {prof.get('username', 'Unknown')}")
                            if prof.get("bio"):
                                print(f"    Bio: {prof['bio']}")
                            print(f"    Visibility: {prof.get('visibility', {})}")
                    else:
                        prof = local_profile.get()
                        print(f"  [Profile] {prof.get('username', 'Unknown')}")
                        if prof.get("bio"):
                            print(f"    Bio: {prof['bio']}")
                        print(f"    Visibility: {prof.get('visibility', {})}")
                except Exception as e:
                    print(f"  [Profile] Error: {e}")
                continue
            elif cmd == "/help":
                print()
                print(f"  {_bold('BRYNQ OMNI-OS COMMANDS:')}" if _COLOR else "  BRYNQ OMNI-OS COMMANDS:")
                
                print(f"\n  {_dim('Core AI & Models:')}" if _COLOR else "\n  Core AI & Models:")
                print("  /models          — list active models")
                print("  /status          — show models and service status")
                print("  /add <model>     — add a model to the team")
                print("  /remove <model>  — remove a model")
                print("  /model <name>    — set default model")
                print("  /use-native      — switch to proprietary Brynq-Coder-v1 base model")
                print("  /compare <msg>   — test a prompt against all active models")
                print("  /benchmark       — run latency benchmarks across models")
                
                print(f"\n  {_dim('Agents & Swarms:')}" if _COLOR else "\n  Agents & Swarms:")
                print("  /chain <prompt>  — run a standard multi-model chain")
                print("  /swarm <task>    — spawn persistent agent team")
                print("  /swarm auto <..> — dynamic delegation to an auto-scaled sub-swarm")
                print("  /workflow run <f>— execute a DAG json workflow")
                print("  /hyperswarm <g>  — trigger recursive, infinite fractal agent spawning")
                print("  /hyperstatus     — view the fractal agent tree")
                print("  /vote <task>     — algorithmic multi-agent consensus selection")
                
                print(f"\n  {_dim('Data & Memory (Exocortex):')}" if _COLOR else "\n  Data & Memory (Exocortex):")
                print("  /index <dir/file>— vectorize and store local code/docs for RAG")
                print("  /rag list        — show indexed files")
                print("  /recall <query>  — search perfect timeline history (TimelineDB)")
                print("  /cache           — view Semantic Neural Cache metrics")
                print("  /sync            — package local Exocortex and sync to Brynq Cloud")
                print("  /global-ask <q>  — query the anonymized Global Brynq Hivemind")
                print("  /build-dataset   — convert local history to JSONL for fine-tuning")

                print(f"\n  {_dim('Security & Privacy:')}" if _COLOR else "\n  Security & Privacy:")
                print("  /scan            — [IMMUNE] real vulnerability/port sweep (Bandit/Safety)")
                print("  /scrub <text>    — test zero-knowledge PII sanitization")
                print("  /simulate <cmd>  — [SANDBOX] run command safely in ephemeral Docker VM")
                print("  /policy sync     — pull enterprise IT fleet rules")

                print(f"\n  {_dim('Physical & Ambient Ops:')}" if _COLOR else "\n  Physical & Ambient Ops:")
                print("  /browser <url>   — launch Playwright autonomous browser")
                print("  /desktop <act>   — trigger PyAutoGUI physical OS control")
                print("  /deploy <dir>    — zero-click AWS Terraform deployment")
                print("  /iot trigger     — send real-world IoT MQTT/HomeAssistant events")
                print("  /listen start    — open continuous audio/video streams")
                print("  /predict         — intent prediction based on local git/log states")
                print("  /empathy         — print current inferred user emotional state")
                print("  /dream start     — begin idle Singularity research loop")
                
                print(f"\n  {_dim('Ecosystem & Marketplace:')}" if _COLOR else "\n  Ecosystem & Marketplace:")
                print("  /marketplace     — browse available tools and agents")
                print("  /search <query>  — find agents in the global marketplace")
                print("  /install <id>    — install an agent from the marketplace")
                print("  /uninstall <id>  — uninstall an installed agent")
                print("  /installed       — view locally downloaded agents")
                print("  /trending        — top downloaded agents")
                print("  /info <id>       — read agent manifest details")
                print("  /build-tool <d>  — instruct Brynq to write a new Python MCP tool")
                print("  /forge <task>    — spawn/breed/mutate genetic agent prompts")

                print(f"\n  {_dim('Rooms & Multiplayer:')}" if _COLOR else "\n  Rooms & Multiplayer:")
                print("  /room create <n> — create a room, get invite code")
                print("  /room join <code>— join a room via invite code")
                print("  /room status     — show room members and compute")
                print("  /room leave      — leave the current room")
                print("  /room close      — close room (host only)")
                print("  /chat <message>  — send message to room")
                print("  /profile set <f> — set profile field (bio, username)")
                print("  /profile show <f>— toggle field visibility (true/false)")

                print(f"\n  {_dim('System & Grid:')}" if _COLOR else "\n  System & Grid:")
                print("  /login <key>     — authenticate identity to Brynq Cloud")
                print("  /grid join       — link your laptop into the local P2P compute grid")
                print("  /wallet          — view local autonomous credit balance")
                print("  /api start       — expose the background FastAPI Omniverse endpoints")
                print("  /schedule <cron> — register a background AI daemon")
                print("  /diagnose        — run connectivity health checks")
                print("  /config          — show runtime configuration")
                print("  /usage           — estimate token usage for session")
                print("  /history         — show last 10 messages")
                print("  /export          — save chat history to JSON")
                print("  /clear           — clear chat history")
                print("  /quit            — exit the Omni-OS")
                print()
                continue
            elif cmd == "/chain":
                if cmd_arg:
                    _run_chain_inline(config, cmd_arg)
                else:
                    print("  Usage: /chain <prompt>")
                continue
            elif cmd == "/workflow":
                if not cmd_arg or not cmd_arg.startswith("run "):
                    print("  Usage: /workflow run <file.json>")
                    continue
                wf_file = cmd_arg[4:].strip()
                try:
                    from runtime.workflows.dag_executor import DAGExecutor
                    data = json.loads(Path(wf_file).read_text("utf-8"))
                    print(f"  Starting DAG workflow from {wf_file}...")
                    executor = DAGExecutor()
                    for step in data.get("steps", []):
                        executor.add_node(step["id"], step.get("agent", "claude"), step["task"])
                    for edge in data.get("edges", []):
                        executor.add_edge(edge["from"], edge["to"])
                    res = executor.execute()
                    print("\n  Workflow Result:")
                    for k, v in res.items():
                        print(f"  [{k}]: {v[:100]}...")
                except Exception as e:
                    print(f"  Workflow error: {e}")
                continue
            elif cmd == "/index":
                if not cmd_arg:
                    print("  Usage: /index <dir_or_file>")
                    continue
                try:
                    from runtime.rag.vector_store import VectorStore
                    from runtime.rag.document_loaders import load_markdown, load_pdf, load_code_dir
                    store = VectorStore()
                    path = Path(cmd_arg)
                    docs = []
                    if path.is_file():
                        if path.suffix == ".md":
                            docs.extend(load_markdown(str(path)))
                        elif path.suffix == ".pdf":
                            docs.append({"text": load_pdf(str(path)), "metadata": {"source": str(path)}})
                        else:
                            docs.append({"text": path.read_text("utf-8", errors="ignore"), "metadata": {"source": str(path)}})
                    elif path.is_dir():
                        docs.extend(load_code_dir(str(path)))
                    store.add_documents([d["text"] for d in docs])
                    print(f"  Indexed {len(docs)} chunks from {cmd_arg} into vector store.")
                except Exception as e:
                    print(f"  Index error: {e}")
                continue
            elif cmd == "/swarm":
                if not cmd_arg:
                    print("  Usage: /swarm <task description>")
                    print("         /swarm auto <task description>")
                    print("  Example: /swarm auto Review all Python files for bugs")
                elif cmd_arg.startswith("auto "):
                    task = cmd_arg[5:].strip()
                    try:
                        from runtime.executor.sub_swarm import SubSwarmManager
                        mgr = SubSwarmManager()
                        print("  Delegating task to dynamic sub-swarm...")
                        res = mgr.delegate_task(task, num_agents=3)
                        final_res = mgr.synthesize_results(res)
                        print("\n  Sub-Swarm Synthesis:")
                        print(final_res)
                    except Exception as e:
                        print(f"  Swarm auto error: {e}")
                else:
                    try:
                        from runtime.spawner import spawn_swarm
                        ollama_models = [m for m in active_models if m.startswith("ollama:")]
                        if not ollama_models:
                            print("  No Ollama models available for swarm. Install one: ollama pull llama3")
                        else:
                            model_name = ollama_models[0].split(":", 1)[1]
                            agents_cfg = [
                                {"name": f"swarm-{i+1}", "backend": "ollama", "model": model_name}
                                for i in range(min(3, len(ollama_models)))
                            ]
                            result = spawn_swarm(
                                swarm_name=f"chat-swarm-{int(time.time())}",
                                task=cmd_arg,
                                agents=agents_cfg,
                            )
                            spawned = [a["name"] for a in result.get("agents", []) if a.get("status") == "running"]
                            print(f"  Swarm launched: {len(spawned)} agents on #{result.get('channel', '?')}")
                            print(f"  Task: {cmd_arg}")
                            print(f"  Agents: {', '.join(spawned)}")
                    except Exception as e:
                        print(f"  Swarm error: {e}")
                continue
            else:
                _redraw()
                print(f"\n  {_dim('Unknown command. Type /help for available commands.')}")
                continue

        # ── Detect CLI commands typed in chat ─────────────────────
        if user_input.strip().startswith("brynq-runtime ") or user_input.strip().startswith("brynq "):
            print()
            print(f"  {_dim('That looks like a terminal command. Run it in PowerShell/Terminal, not inside chat.')}")
            print(f"  {_dim('Or use slash commands: /chain, /help')}")
            print()
            continue

        # ── Self-awareness: answer questions about Brynq itself ─────
        you_label = f"{_WHITE_BOLD}You{_RESET}" if _COLOR else "You"
        _self_answer = _answer_about_brynq(user_input, config, active_models)
        if _self_answer:
            brynq_label = f"{_GREEN_C}Brynq{_RESET}" if _COLOR else "Brynq"
            chat_history.append(("user", you_label, user_input, "[self]"))
            chat_history.append(("assistant", brynq_label, _self_answer, "instant"))
            conversation.append({"role": "user", "content": user_input})
            conversation.append({"role": "assistant", "content": _self_answer})
            _redraw()
            continue

        # ── Auto-install: if prompt mentions a tool we don't have, install it ──
        def _auto_resolve_tool(prompt_text: str) -> tuple[str | None, dict | None]:
            """Find, auto-install, and optionally call a marketplace tool matching the prompt.
            Returns (status_message, agent_manifest) or (None, None) if no match."""
            try:
                # Get installed agents
                agents_dir = config.data_dir / "agents"
                installed = {}  # id -> manifest
                if agents_dir.exists():
                    for mf in agents_dir.glob("*/manifest.json"):
                        try:
                            m = json.loads(mf.read_text(encoding="utf-8"))
                            installed[m.get("agent_id", "")] = m
                        except Exception:
                            pass

                # Extract keywords from prompt
                t = prompt_text.lower()
                _stopwords = {'the','and','for','from','with','that','this','can','you','check',
                              'today','please','want','need','show','get','find','look','help',
                              'what','how','does','about','into','have','make','just','some',
                              'all','any','our','its','are','was','been','will','would','could',
                              'should','them','their','they','your','mine','where','when','which'}
                import re as _re_kw
                _keywords = [w for w in _re_kw.findall(r'\b[a-zA-Z]{3,}\b', t) if w not in _stopwords]
                if not _keywords:
                    return None, None

                # Try local catalog first, fall back to cloud API
                hits = []
                _seen_ids = set()
                try:
                    from cloud.marketplace.catalog import MarketplaceCatalog
                    for data_path in [
                        Path(__file__).resolve().parent.parent / "cloud" / "data",
                        Path.cwd() / "cloud" / "data",
                    ]:
                        if (data_path / "marketplace" / "catalog.json").exists():
                            cat = MarketplaceCatalog(data_path)
                            for kw in _keywords:
                                for h in cat.search(kw, limit=3):
                                    if h.id not in _seen_ids:
                                        _seen_ids.add(h.id)
                                        hits.append(h)
                                if len(hits) >= 10:
                                    break
                            break
                except ImportError:
                    pass

                # Fall back to cloud API if no local catalog
                if not hits:
                    try:
                        search_q = " ".join(_keywords[:3])
                        resp = cloud.list_marketplace_agents(search=search_q)
                        if resp:
                            from types import SimpleNamespace
                            for agent_info in resp[:5]:
                                hit = SimpleNamespace(
                                    id=getattr(agent_info, 'agent_id', '') or agent_info.get('agent_id', '') if isinstance(agent_info, dict) else str(agent_info),
                                    name=getattr(agent_info, 'name', '') if hasattr(agent_info, 'name') else '',
                                    description=getattr(agent_info, 'description', '') if hasattr(agent_info, 'description') else '',
                                    install_type='mcp_stdio',
                                    mcp_command=None,
                                    endpoint=None,
                                    category='',
                                )
                                if hit.id not in _seen_ids:
                                    _seen_ids.add(hit.id)
                                    hits.append(hit)
                    except Exception:
                        pass

                for hit in hits:
                    # Check relevance: tool name/description words overlap with prompt keywords
                    name_words = hit.name.lower().replace("-", " ").replace("_", " ").split()
                    desc_words = hit.description.lower().split()[:10] if hit.description else []
                    all_words = name_words + desc_words
                    if not any(w in t for w in all_words if len(w) > 2):
                        continue

                    # Already installed? Return manifest directly
                    if hit.id in installed:
                        return f"Using installed tool: {hit.name}", installed[hit.id]

                    # Auto-install
                    agent_dir = config.data_dir / "agents" / hit.id
                    agent_dir.mkdir(parents=True, exist_ok=True)
                    manifest = {
                        "agent_id": hit.id,
                        "name": hit.name,
                        "description": hit.description,
                        "install_type": hit.install_type,
                        "mcp_command": hit.mcp_command,
                        "endpoint": hit.endpoint,
                        "category": hit.category,
                        "auto_installed": True,
                    }
                    (agent_dir / "manifest.json").write_text(
                        json.dumps(manifest, indent=2), encoding="utf-8"
                    )
                    return f"Auto-installed: {hit.name}", manifest
            except Exception:
                pass
            return None, None

        def _log_tool_usage(agent_id: str, name: str, action: str, success: bool, elapsed_ms: float = 0):
            """Log tool install/call to ~/.brynq/tool_usage.jsonl"""
            try:
                from datetime import datetime, timezone
                log_file = config.data_dir / "tool_usage.jsonl"
                log_file.parent.mkdir(parents=True, exist_ok=True)
                entry = json.dumps({
                    "timestamp": datetime.now(timezone.utc).isoformat(),
                    "agent_id": agent_id,
                    "name": name,
                    "action": action,  # "auto_install", "call", "call_failed"
                    "success": success,
                    "elapsed_ms": round(elapsed_ms),
                })
                with open(log_file, "a", encoding="utf-8") as f:
                    f.write(entry + "\n")
            except Exception:
                pass

        tool_msg, tool_manifest = _auto_resolve_tool(user_input)
        if tool_msg:
            print(f"  {_dim(f'[{tool_msg}]')}" if _COLOR else f"  [{tool_msg}]")
            if tool_manifest and "Auto-installed" in (tool_msg or ""):
                _log_tool_usage(tool_manifest.get("agent_id", ""), tool_manifest.get("name", ""), "auto_install", True)
            # If we have an MCP tool, try to call it directly
            if tool_manifest and tool_manifest.get("install_type") == "mcp_stdio" and tool_manifest.get("mcp_command"):
                _tool_start = time.time()
                try:
                    from runtime.mcp_adapter import MCPServerConnection, MCPAgentAdapter
                    with MCPServerConnection(command=tool_manifest["mcp_command"]) as conn:
                        conn.connect()
                        adapter = MCPAgentAdapter(conn, tool_manifest["agent_id"], tool_manifest["name"])
                        result = adapter.execute(user_input, {}, {})
                        _tool_elapsed = (time.time() - _tool_start) * 1000
                        if result.get("output"):
                            _log_tool_usage(tool_manifest["agent_id"], tool_manifest["name"], "call", True, _tool_elapsed)
                            brynq_label = f"{_GREEN_C}{tool_manifest['name']}{_RESET}" if _COLOR else tool_manifest["name"]
                            chat_history.append(("user", you_label, user_input, f"[tool: {tool_manifest['name']}]"))
                            chat_history.append(("assistant", brynq_label, result["output"], f"{result.get('elapsed_ms', 0):.0f}ms"))
                            conversation.append({"role": "user", "content": user_input})
                            conversation.append({"role": "assistant", "content": result["output"]})
                            _redraw()
                            continue
                except FileNotFoundError:
                    _tool_elapsed = (time.time() - _tool_start) * 1000
                    _log_tool_usage(tool_manifest.get("agent_id", ""), tool_manifest.get("name", ""), "call_failed", False, _tool_elapsed)
                    cmd_name = tool_manifest.get("mcp_command", ["?"])[0]
                    if cmd_name in ("npx", "npx.cmd"):
                        print(f"  {_dim('[Node.js not found. Install from https://nodejs.org to use marketplace tools.]')}" if _COLOR else "  [Install Node.js from nodejs.org to use marketplace tools]")
                    else:
                        print(f"  {_dim(f'[Command not found: {cmd_name}. Routing to AI models instead.]')}" if _COLOR else f"  [Command not found: {cmd_name}]")
                except Exception as e:
                    _tool_elapsed = (time.time() - _tool_start) * 1000
                    _log_tool_usage(tool_manifest.get("agent_id", ""), tool_manifest.get("name", ""), "call_failed", False, _tool_elapsed)
                    print(f"  {_dim(f'[Tool call failed: {e}. Routing to AI models instead.]')}" if _COLOR else f"  [Tool call failed, using models]")

        # ── Classify and orchestrate ───────────────────────────────
        task_type, strategy, reason = _classify(user_input)

        # Intercept if prompt matches an installed tool name
        installed = _get_installed_agents(config.data_dir)
        matched_tool_agent = None
        matched_tool_name = None
        for agent in installed:
            for t in agent.get("tools", []):
                if t.get("name") and t.get("name").lower() in user_input.lower():
                    matched_tool_agent = agent.get("agent_id") or agent.get("name")
                    matched_tool_name = t.get("name")
                    break
            if matched_tool_agent:
                break
        
        if matched_tool_agent:
            print(f"  {_dim(f'[Using installed tool: {matched_tool_name} from {matched_tool_agent}]')}")
            # We could route directly to the agent here, but for MVP we will just let the orchestrator 
            # proceed with the hint displayed. In a full implementation, we'd wrap this execution.
            
        # Add to conversation memory
        conversation.append({"role": "user", "content": user_input})

        you_label = f"{_WHITE_BOLD}You{_RESET}" if _COLOR else "You"

        # Try direct action first (create folder, open URL, etc.)
        if task_type == "action":
            direct_result = _try_direct_action(user_input)
            if direct_result:
                brynq_label = f"{_GREEN_C}Brynq{_RESET}" if _COLOR else "Brynq"
                chat_history.append(("user", you_label, user_input, "[action → direct]"))
                chat_history.append(("assistant", brynq_label, direct_result, "instant"))
                conversation.append({"role": "assistant", "content": direct_result})
                _redraw()
                continue

        # RAG context injection
        rag_prefix = ""
        if _rag_enabled:
            try:
                from runtime.rag import query, build_context
                chunks = query(config.data_dir, user_input, top_k=3)
                if chunks:
                    rag_prefix = build_context(chunks) + "\n\n"
            except Exception:
                pass

        # File auto-reading: detect file paths in user input and inline contents
        file_prefix = ""
        referenced_files = _scan_file_paths(user_input)
        if referenced_files:
            file_prefix = _prepend_file_contents("", referenced_files).strip() + "\n\n"
            names = ", ".join(fp.name for fp in referenced_files)
            print(f"  {_DIM}[Auto-read: {names}]{_RESET}" if _COLOR else f"  [Auto-read: {names}]")

        # Route to models
        models_for_task = _pick_models(task_type, strategy)
        strategy_label = f"[{task_type} → {strategy}]"
        if rag_prefix:
            strategy_label += " +RAG"
        if referenced_files:
            strategy_label += f" +{len(referenced_files)} file{'s' if len(referenced_files) != 1 else ''}"
        chat_history.append(("user", you_label, user_input, strategy_label))
        _redraw()
        _print_status(f"Strategy: {strategy} | Task: {task_type} | Models: {', '.join(models_for_task)}")
        if verbose:
            _print_status(f"Reason: {reason}")
        # Prepend file contents + RAG context to user input for models
        model_input = file_prefix + rag_prefix + user_input if (file_prefix or rag_prefix) else user_input

        t_start = time.time()
        if strategy == "debate":
            if len(models_for_task) < 2:
                print(f"  {_DIM}[Only 1 model available, using single strategy]{_RESET}")
                strategy = "single"
                _run_single(model_input, models_for_task)
            else:
                _run_debate(model_input, models_for_task)
        elif strategy == "refine":
            _run_refine(model_input, models_for_task)
        elif strategy == "fan_out":
            _run_fan_out(model_input, models_for_task)
        elif strategy == "swarm":
            _run_swarm(model_input, models_for_task)
        else:
            _run_single(model_input, models_for_task)

        # Track interaction for usage analytics
        t_elapsed = time.time() - t_start
        try:
            last_result = chat_history[-1][2] if chat_history else ""
            primary_model = models_for_task[0] if models_for_task else "unknown"
            _track_interaction(primary_model, strategy, user_input, last_result, t_elapsed, True)
        except Exception:
            pass  # Never let tracking break the chat loop

        # Save last response to conversation memory
        if chat_history:
            last = chat_history[-1]
            conversation.append({"role": "assistant", "content": last[2][:1000]})

        # Auto-save: extract code blocks and save to files
        if task_type == "code" and chat_history:
            _auto_save_code(chat_history)

        _redraw()

    return 0  # pragma: no cover


def _run_chain_inline(config: RuntimeConfig, prompt: str) -> None:
    """Execute a chain and print results inline in chat."""
    if not _is_runtime_running(config):
        print(f"  {_red('Error')}: Chains require the runtime server. Run: brynq-runtime start")
        return

    print()
    print(f"  {_dim('Running chain...')}")
    start = time.time()

    try:
        result = _runtime_post(config, "/chain", {"request": prompt})
        total_ms = result.get("total_duration_ms", 0)
        steps = result.get("steps", [])
        success = result.get("success", False)

        print()
        for i, step in enumerate(steps, 1):
            model = step.get("model", "unknown")
            content = step.get("content", "")
            step_ok = step.get("success", False)
            status = _green("done") if step_ok else _red("failed")

            print(f"  Step {i} [{_cyan(model)}] {status}")
            print(f"  {content[:200]}{'...' if len(content) > 200 else ''}")
            print()

        print(f"  {_dim(f'Chain complete: {len(steps)} steps, {total_ms}ms total')}")
        print()

    except urllib.error.HTTPError as e:
        try:
            error_body = json.loads(e.read().decode("utf-8"))
            detail = error_body.get("detail", str(e))
        except Exception:
            detail = str(e)
        print(f"  {_red('Error')}: {detail}")
        print()

    except Exception as e:
        print(f"  {_red('Error')}: {e}")
        print()


def cmd_chain(args: argparse.Namespace, config: RuntimeConfig) -> int:
    """Run a multi-model chain from the terminal."""
    if not _is_runtime_running(config):
        print(f"{_red('Error')}: Runtime is not running. Start it first:")
        print(f"  brynq-runtime start")
        return 1

    prompt = args.prompt
    print(f"{_bold('Chain')}: {prompt}")
    print()

    start = time.time()

    try:
        result = _runtime_post(config, "/chain", {"request": prompt})
    except urllib.error.HTTPError as e:
        try:
            error_body = json.loads(e.read().decode("utf-8"))
            detail = error_body.get("detail", str(e))
        except Exception:
            detail = str(e)
        print(f"{_red('Error')}: {detail}")
        return 1
    except Exception as e:
        print(f"{_red('Error')}: Could not reach runtime: {e}")
        return 1

    total_ms = result.get("total_duration_ms", 0)
    steps = result.get("steps", [])
    success = result.get("success", False)

    for i, step in enumerate(steps, 1):
        model = step.get("model", "unknown")
        content = step.get("content", "")
        step_ok = step.get("success", False)
        error = step.get("error")

        status = _green("done") if step_ok else _red("failed")
        print(f"  Step {i}/{len(steps)} [{_cyan(model)}] {status}")
        print()

        if content:
            # Indent content
            for line in content.splitlines():
                print(f"    {line}")
            print()

        if error:
            print(f"    {_red('Error')}: {error}")
            print()

    # Summary
    wall_ms = int((time.time() - start) * 1000)
    status_str = _green("success") if success else _red("failed")
    print(f"  {_dim('---')}")
    print(f"  Result: {status_str}  |  Steps: {len(steps)}  |  Time: {total_ms}ms (wall: {wall_ms}ms)")

    # Email results if requested
    email_addr = getattr(args, "email", None)
    if email_addr:
        from runtime.integrations.email_sender import EmailConfig, send_chain_result

        email_config = EmailConfig()
        sent = send_chain_result(email_config, email_addr, result)
        if sent:
            print(f"Results emailed to {email_addr}.")
        else:
            print(f"{_red('Error')}: Failed to email results to {email_addr}.")

    return 0 if success else 1


def cmd_batch(args: argparse.Namespace, config: RuntimeConfig) -> int:
    """Process a batch file of prompts through the runtime."""
    import asyncio as _asyncio

    from runtime.integrations.batch import (
        BatchProgress,
        load_batch_file,
        run_batch,
        write_batch_results,
    )

    if not _is_runtime_running(config):
        print(f"{_red('Error')}: Runtime is not running. Start it first:")
        print("  brynq-runtime start")
        return 1

    # Load prompts
    filepath = args.filepath
    try:
        prompts = load_batch_file(filepath)
    except FileNotFoundError as exc:
        print(f"{_red('Error')}: {exc}")
        return 1
    except ValueError as exc:
        print(f"{_red('Error')}: {exc}")
        return 1

    if not prompts:
        print(f"{_yellow('Warning')}: No prompts found in {filepath}")
        return 0

    model = getattr(args, "model", None)
    strategy = getattr(args, "strategy", None)
    output_path = getattr(args, "output", None)
    fmt = getattr(args, "format", "json") or "json"

    print(f"{_bold('Batch')}: {filepath}")
    print(f"  Prompts: {len(prompts)}")
    if model:
        print(f"  Model:   {model}")
    if strategy:
        print(f"  Strategy: {strategy}")
    print()

    endpoint_url = f"http://127.0.0.1:{config.local_port}/chat"
    progress = BatchProgress(len(prompts))

    start = time.time()
    try:
        results = _asyncio.run(
            run_batch(prompts, endpoint_url, model=model, strategy=strategy)
        )
    except Exception as exc:
        print(f"{_red('Error')}: Batch execution failed: {exc}")
        return 1

    wall_s = time.time() - start

    # Update progress with final counts
    failed = sum(1 for r in results if r.get("status", 0) != 200)
    succeeded = len(results) - failed
    progress.update(succeeded, failed)

    # Write output file if requested
    if output_path:
        try:
            write_batch_results(results, output_path, fmt)
            print(f"  Output written to {output_path} ({fmt})")
        except ValueError as exc:
            print(f"{_red('Error')}: {exc}")
            return 1

    # Print summary
    print(f"\n  {_dim('---')}")
    print(f"  Total:     {len(results)}")
    print(f"  Succeeded: {_green(str(succeeded))}")
    if failed:
        print(f"  Failed:    {_red(str(failed))}")
    avg_latency = (
        sum(r.get("latency_ms", 0) for r in results) / len(results)
        if results
        else 0
    )
    print(f"  Avg latency: {avg_latency:.0f}ms")
    print(f"  Wall time:   {wall_s:.1f}s")

    return 0 if failed == 0 else 1


def cmd_setup(args: argparse.Namespace, config: RuntimeConfig) -> int:
    """Create a desktop shortcut for Brynq Chat."""
    import shutil

    desktop = Path.home() / "Desktop"
    if not desktop.exists():
        # Try XDG desktop dir (Linux)
        desktop = Path.home()

    python = sys.executable

    if sys.platform == "win32":
        # Windows: create a .bat file
        bat_path = desktop / "Brynq Chat.bat"
        bat_path.write_text(
            f'@echo off\ntitle Brynq Chat\n"{python}" -m runtime chat\n',
            encoding="utf-8",
        )
        print(f"  {_green('Created:')} {bat_path}")
        print(f"  Double-click it to start Brynq Chat.")

    elif sys.platform == "darwin":
        # macOS: create a .command file
        cmd_path = desktop / "Brynq Chat.command"
        cmd_path.write_text(
            f'#!/bin/bash\n"{python}" -m runtime chat\n',
            encoding="utf-8",
        )
        cmd_path.chmod(0o755)
        print(f"  {_green('Created:')} {cmd_path}")

    else:
        # Linux: create a .desktop file
        desktop_path = desktop / "brynq-chat.desktop"
        desktop_path.write_text(
            f"[Desktop Entry]\n"
            f"Name=Brynq Chat\n"
            f"Comment=Multi-model AI chat\n"
            f"Exec={python} -m runtime chat\n"
            f"Terminal=true\n"
            f"Type=Application\n"
            f"Categories=Development;\n",
            encoding="utf-8",
        )
        desktop_path.chmod(0o755)
        print(f"  {_green('Created:')} {desktop_path}")

    return 0


def cmd_completions(args: argparse.Namespace, config: RuntimeConfig) -> int:
    """Print or install shell completion scripts."""
    from runtime.integrations.shell_completion import (
        generate_zsh_completion,
        generate_powershell_completion,
    )

    shell = args.shell

    if args.install:
        from runtime.integrations.shell_completion import install_completion

        install_completion(shell)
        return 0

    generators = {
        "zsh": generate_zsh_completion,
        "powershell": generate_powershell_completion,
    }

    gen = generators.get(shell)
    if gen is None:
        print(f"  {_red('Error:')} No completion script available for '{shell}'.")
        print(f"  Supported shells: {', '.join(generators)}")
        return 1

    print(gen())
    return 0


def cmd_monitor(args: argparse.Namespace, config: RuntimeConfig) -> int:
    """Live htop-style monitor for agents and chains."""
    from runtime.monitor import run_monitor
    run_monitor(config, interval=getattr(args, "interval", 2.0))
    return 0


def cmd_dashboard(args: argparse.Namespace, config: RuntimeConfig) -> int:
    """Open the operator dashboard in browser."""
    import threading
    try:
        from brynq.dashboard import run_dashboard
    except ImportError:
        print(f"{_red('Error')}: Dashboard requires full install: pip install -e '.[all]'")
        return 1
    if not getattr(args, "no_browser", False):
        import webbrowser
        port = getattr(args, "port", 9999)
        threading.Timer(0.8, lambda: webbrowser.open(f"http://127.0.0.1:{port}")).start()
    port = getattr(args, "port", 9999)
    print(f"  Dashboard: http://127.0.0.1:{port}")
    run_dashboard(port=port)
    return 0


def cmd_tui(args: argparse.Namespace, config: RuntimeConfig) -> int:
    """Launch the TUI chat (rich terminal UI with panels)."""
    try:
        from runtime.tui_chat import BrynqChat
        BrynqChat(config).run()
        return 0
    except ImportError:
        print(f"{_red('Error')}: TUI requires textual: pip install textual")
        return 1


def cmd_spawn(args: argparse.Namespace, config: RuntimeConfig) -> int:
    """Spawn AI agents to work on a task."""
    from runtime.spawner import spawn_agent, spawn_swarm, list_agents, kill_agent

    sub = getattr(args, "spawn_command", None)

    if sub == "list":
        agents = list_agents(config.data_dir)
        if not agents:
            print("  No agents running.")
        for a in agents:
            status = _green("alive") if a.get("alive") else _red("dead")
            print(f"  {a['name']:20s}  {a.get('backend','?'):8s}  {a.get('model','?'):15s}  {status}")
        return 0

    elif sub == "kill":
        name = getattr(args, "name", "")
        if kill_agent(config.data_dir, name):
            print(f"  Killed: {name}")
        else:
            print(f"  Not found: {name}")
        return 0

    elif sub == "run":
        task = getattr(args, "task", "")
        count = getattr(args, "count", 1)
        model = getattr(args, "model", "llama3")
        backend = getattr(args, "backend", "ollama")

        if count == 1:
            result = spawn_agent(
                config.data_dir,
                name=f"agent-{model}",
                backend=backend,
                model=model,
                channel="tasks",
                system_prompt=task,
            )
            print(f"  Spawned: {result.get('name', '?')} (PID {result.get('pid', '?')})")
        else:
            agents_config = [
                {"name": f"agent-{model}-{i+1}", "backend": backend, "model": model}
                for i in range(count)
            ]
            results = spawn_swarm(
                config.data_dir,
                swarm_name=f"swarm-{model}",
                task=task,
                agents=agents_config,
            )
            print(f"  Swarm spawned: {len(results)} agents on #swarm-{model}")
            for r in results:
                print(f"    {r.get('name', '?')} (PID {r.get('pid', '?')})")
        return 0

    else:
        print("  Usage: python -m runtime spawn run --task 'research X' --count 3")
        print("         python -m runtime spawn list")
        print("         python -m runtime spawn kill <name>")
        return 0


def cmd_workflow(args: argparse.Namespace, config: RuntimeConfig) -> int:
    """Manage and run DAG workflows."""
    from runtime.workflow_store import (
        create_workflow, start_workflow, get_run,
        list_runs, list_workflows,
    )

    sub = getattr(args, "workflow_command", None)

    if sub == "create":
        filepath = getattr(args, "file", "")
        if not filepath:
            print("  Usage: python -m runtime workflow create --file workflow.json")
            return 1
        data = json.loads(Path(filepath).read_text("utf-8"))
        wf = create_workflow(
            config.data_dir,
            name=data.get("name", "unnamed"),
            description=data.get("description", ""),
            steps=data.get("steps", []),
        )
        print(f"  Created workflow: {wf['workflow_id']}")
        return 0

    elif sub == "run":
        wf_id = getattr(args, "id", "")
        user_input = getattr(args, "input", "")
        run = start_workflow(config.data_dir, wf_id, user_input)
        print(f"  Run started: {run['run_id']}")
        return 0

    elif sub == "status":
        run_id = getattr(args, "run_id", "")
        run = get_run(config.data_dir, run_id)
        if not run:
            print(f"  Run not found: {run_id}")
            return 1
        print(f"  Run: {run_id}")
        print(f"  Status: {run.get('status', '?')}")
        for sid, step in run.get("step_statuses", {}).items():
            print(f"    {sid}: {step.get('status', '?')}")
        return 0

    elif sub == "list":
        workflows = list_workflows(config.data_dir)
        if not workflows:
            print("  No workflows defined.")
        for wf in workflows:
            print(f"  {wf['workflow_id']:12s}  {wf.get('name', '?')}")
        return 0

    else:
        print("  Usage: python -m runtime workflow create --file workflow.json")
        print("         python -m runtime workflow run <id> --input 'prompt'")
        print("         python -m runtime workflow status <run_id>")
        print("         python -m runtime workflow list")
        return 0


def _format_pipe_output(result_dict: dict, fmt: str) -> str:
    """Format pipe output as text, json, csv, or tsv."""
    if fmt == "json":
        import json as json_mod
        return json_mod.dumps(result_dict)
    elif fmt == "csv":
        headers = list(result_dict.keys())
        values = [str(v) if v is not None else "" for v in result_dict.values()]
        return ",".join(headers) + "\n" + ",".join(values)
    elif fmt == "tsv":
        headers = list(result_dict.keys())
        values = [str(v) if v is not None else "" for v in result_dict.values()]
        return "\t".join(headers) + "\n" + "\t".join(values)
    else:
        # text: just the output field
        return str(result_dict.get("output", ""))


def cmd_pipe(args: argparse.Namespace, config: RuntimeConfig) -> int:
    """Headless pipe mode. Reads stdin, sends to /chat, writes to stdout."""
    import socket
    import sys
    import time
    import urllib.error

    if sys.stdin.isatty():
        print("Error: No input provided on stdin. Usage: echo 'prompt' | brynq-runtime pipe", file=sys.stderr)
        return 1

    if not _is_runtime_running(config):
        print("Error: Runtime server is not running. Start it with 'brynq-runtime start'", file=sys.stderr)
        return 1

    raw = sys.stdin.read().strip()
    if not raw:
        return 0

    delimiter = getattr(args, "delimiter", "\n")
    chunks = [c.strip() for c in raw.split(delimiter) if c.strip()]
    if not chunks:
        return 0

    # Batch chunks: collect N lines into a single prompt
    _bs = getattr(args, "batch_size", 1)
    batch_size = _bs if isinstance(_bs, int) and _bs > 1 else 1
    if batch_size > 1:
        batched: list[str] = []
        for i in range(0, len(chunks), batch_size):
            batched.append("\n".join(chunks[i : i + batch_size]))
        chunks = batched

    # Determine output format once: --format takes precedence, --json is shorthand
    fmt = getattr(args, "format", None)
    if fmt is None and getattr(args, "json", False):
        fmt = "json"

    timeout = getattr(args, "timeout", 120)

    errors = 0
    for text in chunks:
        payload: dict[str, Any] = {"message": text}
        if getattr(args, "model", None):
            payload["model"] = args.model
        if getattr(args, "strategy", None):
            payload["strategy"] = args.strategy

        try:
            start = time.monotonic()
            result = _runtime_post(config, "/chat", payload, timeout=timeout)
            elapsed_ms = int((time.monotonic() - start) * 1000)

            # Extract response text from steps format
            steps = result.get("steps", [])
            response_text = steps[0]["content"] if steps else result.get("response", result.get("content", ""))

            if fmt and fmt != "text":
                model_used = getattr(args, "model", None) or (steps[0].get("model") if steps else None)
                tokens_used = (steps[0].get("tokens") if steps else None) or result.get("tokens")
                result_dict = {
                    "input": text,
                    "output": response_text,
                    "model": model_used,
                    "tokens": tokens_used,
                    "latency_ms": elapsed_ms,
                }
                print(_format_pipe_output(result_dict, fmt))
            else:
                print(response_text)

        except (urllib.error.URLError, socket.timeout) as e:
            is_timeout = isinstance(e, socket.timeout) or (
                isinstance(e, urllib.error.URLError) and isinstance(e.reason, socket.timeout)
            )
            if is_timeout:
                import json as json_mod
                print(json_mod.dumps({"error": "timeout", "input": text}))
                errors += 1
            else:
                print(f"Error: {e}", file=sys.stderr)
                errors += 1
        except Exception as e:
            print(f"Error: {e}", file=sys.stderr)
            errors += 1

    return 1 if errors == len(chunks) else 0


def cmd_watch(args: argparse.Namespace, config: RuntimeConfig) -> int:
    """Watch a directory for new files and process them through a chain."""
    from runtime.integrations.file_watcher import FileWatcher

    directory = args.directory
    if not os.path.isdir(directory):
        print(f"Error: '{directory}' is not a directory", file=sys.stderr)
        return 1

    pattern = getattr(args, "pattern", "*")
    chain = getattr(args, "chain", "default")
    interval = getattr(args, "interval", 5)

    watcher = FileWatcher(directory, interval=float(interval))
    watcher.register_pattern(pattern, chain)
    watcher.start()

    print(f"Watching {directory} for '{pattern}' files (interval: {interval}s)")
    print("Press Ctrl+C to stop.")

    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        watcher.stop()
        print("\nStopped watching.")

    return 0


def cmd_connect(args: argparse.Namespace, config: RuntimeConfig) -> int:
    """Mount a new protocol adapter via AdapterManager."""
    from runtime.adapters.manager import AdapterManager

    url: str = args.url
    manager = AdapterManager()

    try:
        adapter = manager.create_adapter(url)
    except ValueError as exc:
        print(f"Error: {exc}", file=sys.stderr)
        return 1

    try:
        info = adapter.introspect()
    except Exception as exc:
        print(f"Adapter created but introspect failed: {exc}", file=sys.stderr)
        return 1

    tools = info.get("tools", [])
    version = info.get("version", "unknown")
    print(_green(f"Connected to {url}"))
    print(f"  Adapter version: {version}")
    print(f"  Tools available: {len(tools)}")
    for tool in tools:
        name = tool.get("name", "?")
        desc = tool.get("description", "")
        print(f"    - {name}: {desc}")

    return 0


def cmd_room(args, config) -> int:
    import asyncio
    from runtime.rooms.room_manager import room_manager
    if getattr(args, "room_cmd") == "create":
        res = asyncio.run(room_manager.create_room(args.name))
        print(f"Room Created! Code: {res['room_code']}")
    elif getattr(args, "room_cmd") == "join":
        res = asyncio.run(room_manager.join_room(args.code))
        print(f"Joined Room! Connected to host: {res['host_address']}")
    elif getattr(args, "room_cmd") == "close":
        asyncio.run(room_manager.close_room())
        print("Room closed.")
    elif getattr(args, "room_cmd") == "leave":
        asyncio.run(room_manager.leave_room())
        print("Left room.")
    elif getattr(args, "room_cmd") == "chat":
        from runtime.rooms.room_manager import room_manager
        room_manager.send_message(" ".join(args.message))
        print("Message sent.")
    return 0

def cmd_profile(args, config) -> int:
    from runtime.rooms.profile import local_profile
    if getattr(args, "bio", None):
        local_profile.update(bio=args.bio)
        print("Bio updated.")
    elif getattr(args, "show_compute") is not None:
        val = args.show_compute.lower() == "true"
        local_profile.update(visibility={"compute": val})
        print(f"Compute visibility set to {val}.")
    return 0


# ── CLI Parser ────────────────────────────────────────────────────────


def build_parser() -> argparse.ArgumentParser:
    """Build the argument parser for the brynq-runtime CLI."""
    parser = argparse.ArgumentParser(
        prog="brynq-runtime",
        description="Brynq Runtime -- Launch and control the local AI runtime.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=(
            "examples:\n"
            "  brynq-runtime start              Start runtime + open browser\n"
            "  brynq-runtime login claude        OAuth login for Claude\n"
            "  brynq-runtime models              List available models\n"
            "  brynq-runtime chat                Interactive terminal chat\n"
            '  brynq-runtime chain "summarize"   Run a multi-model chain\n'
            "  brynq-runtime status              Show runtime status\n"
            "  brynq-runtime stop                Stop the runtime server\n"
        ),
    )

    sub = parser.add_subparsers(dest="command", metavar="command")

    # start
    p_start = sub.add_parser("start", help="Start the runtime server and open the app")
    p_start.add_argument(
        "--no-browser", action="store_true",
        help="Do not open the browser automatically",
    )

    # stop
    sub.add_parser("stop", help="Stop the runtime server")

    # status
    sub.add_parser("status", help="Show runtime status")

    # models
    sub.add_parser("models", help="List available models (local + cloud)")

    # login
    p_login = sub.add_parser("login", help="OAuth login for a cloud provider")
    p_login.add_argument(
        "provider",
        help="Provider name: claude, gemini, openai (or: anthropic, google)",
    )

    # chat
    p_chat = sub.add_parser("chat", help="Interactive terminal chat")
    p_chat.add_argument("--model", "-m", default=None, help="Model to use (auto-detected if omitted)")
    p_chat.add_argument("--once", action="store_true", help="Non-interactive: run one prompt and exit")
    p_chat.add_argument("--verbose", "-v", action="store_true", help="Show strategy selection reasoning")
    p_chat.add_argument("--clipboard", action="store_true", help="Read prompt from clipboard when no prompt given")
    p_chat.add_argument("prompt", nargs="*", default=[], help="Prompt text (for --once mode)")

    # chain
    p_chain = sub.add_parser("chain", help="Run a multi-model chain")
    p_chain.add_argument("prompt", help="Chain prompt describing what to do")
    p_chain.add_argument("--email", default=None, help="Email address to send results to after chain execution")

    # batch
    p_batch = sub.add_parser("batch", help="Process a batch file of prompts")
    p_batch.add_argument("filepath", help="Path to batch file (.txt, .csv, .jsonl)")
    p_batch.add_argument("--model", "-m", default=None, help="Model to use")
    p_batch.add_argument("--strategy", "-s", default=None, choices=["sequential", "fan-out", "debate", "refine", "single"], help="Chain strategy")
    p_batch.add_argument("--output", "-o", default=None, help="Output file path for results")
    p_batch.add_argument("--format", "-f", default="json", choices=["json", "csv", "jsonl"], help="Output format (default: json)")

    # pipe
    p_pipe = sub.add_parser("pipe", help="Headless pipe mode: reads stdin, writes stdout")
    p_pipe.add_argument("--model", "-m", help="Force a specific model")
    p_pipe.add_argument("--strategy", "-s", choices=["sequential", "fan-out", "debate", "refine", "single"], help="Chain strategy")
    p_pipe.add_argument("--json", action="store_true", help="Output structured JSON")
    p_pipe.add_argument("--format", choices=["text", "json", "csv", "tsv"], default=None, help="Output format: text, json, csv, tsv")
    p_pipe.add_argument("--delimiter", default="\n", help="Delimiter to split stdin into chunks (default: newline)")
    p_pipe.add_argument("--timeout", type=int, default=600, help="HTTP request timeout in seconds (default: 120)")
    p_pipe.add_argument("--batch-size", type=int, default=1, help="Number of lines to collect before sending as a single prompt (default: 1)")

    # watch
    p_watch = sub.add_parser("watch", help="Watch a directory for new files and process them")
    p_watch.add_argument("directory", help="Directory to watch for new files")
    p_watch.add_argument("--pattern", default="*", help="Glob pattern to match files (default: '*')")
    p_watch.add_argument("--chain", default="default", help="Chain template name to use")
    p_watch.add_argument("--interval", type=int, default=5, help="Polling interval in seconds (default: 5)")

    # learning commands
    sub.add_parser("stats", help="Show learning subsystem usage statistics")
    sub.add_parser("insights", help="Show AI workflow insights and patterns")
    sub.add_parser("learn", help="Auto-detect preferences based on recent usage")
    p_prefs = sub.add_parser("preferences", help="Manage Brynq learning preferences")
    p_prefs.add_argument("--set", help="Set a preference (key=value)")
    sub.add_parser("reset-learning", help="Clear all local learning data")
    sub.add_parser("trends", help="Show 30-day trends")
    p_costs = sub.add_parser("costs", help="Show cost tracker report")
    p_costs.add_argument("--budget", type=float, help="Set a daily budget limit in USD")
    p_ab = sub.add_parser("ab", help="Manage A/B tests")
    p_ab.add_argument("--create", help="Create a test (name)")
    p_ab.add_argument("--conclude", help="Conclude a test (name)")
    p_ab.add_argument("--delete", help="Delete a test (name)")

    # v6 community commands
    sub.add_parser("network", help="Show network insights (requires telemetry opt-in)")
    sub.add_parser("leaderboard", help="Show model leaderboard")
    p_rate = sub.add_parser("rate", help="Rate an agent")
    p_rate.add_argument("agent_id", help="Agent ID")
    p_rate.add_argument("--rating", type=int, required=True, help="Rating (1-5)")
    p_rate.add_argument("--review", default="", help="Optional review text")
    p_reviews = sub.add_parser("reviews", help="Show reviews for an agent")
    p_reviews.add_argument("agent_id", help="Agent ID")

    # connect
    p_connect = sub.add_parser("connect", help="Mount a new protocol adapter by URL")
    p_connect.add_argument("url", help="Adapter URL (e.g. mcp://localhost:9000, rest://api.example.com)")

    # setup
    sub.add_parser("setup", help="Create a desktop shortcut for Brynq Chat")

    # room
    p_room = sub.add_parser("room", help="Multiplayer room commands")
    room_sub = p_room.add_subparsers(dest="room_cmd")
    p_rc = room_sub.add_parser("create", help="Create a room")
    p_rc.add_argument("name", help="Room name")
    p_rj = room_sub.add_parser("join", help="Join a room")
    p_rj.add_argument("code", help="Room code BRYNQ-XXXX")
    room_sub.add_parser("leave", help="Leave current room")
    room_sub.add_parser("close", help="Close room (host only)")
    p_rm = room_sub.add_parser("chat", help="Send chat message to room")
    p_rm.add_argument("message", nargs="+", help="Message text")

    # profile
    p_prof = sub.add_parser("profile", help="Manage multi-player profile")
    p_prof.add_argument("--bio", help="Set biography")
    p_prof.add_argument("--show-compute", choices=["true", "false"], help="Toggle compute sharing")

    # completions
    p_comp = sub.add_parser("completions", help="Print or install shell completion scripts")
    p_comp.add_argument("shell", choices=["bash", "zsh", "powershell"], help="Target shell")
    p_comp.add_argument("--install", action="store_true", help="Install the completion script into your shell config")

    # monitor
    p_mon = sub.add_parser("monitor", help="Live htop-style process monitor")
    p_mon.add_argument("--interval", "-i", type=float, default=2.0, help="Refresh interval (default: 2s)")

    # dashboard
    p_dash = sub.add_parser("dashboard", help="Open operator dashboard in browser")
    p_dash.add_argument("--port", type=int, default=9999, help="Dashboard port (default: 9999)")
    p_dash.add_argument("--no-browser", action="store_true")

    # tui
    sub.add_parser("tui", help="Rich terminal UI chat (requires textual)")

    # spawn
    p_spawn = sub.add_parser("spawn", help="Spawn AI agents")
    spawn_sub = p_spawn.add_subparsers(dest="spawn_command")
    p_spawn_run = spawn_sub.add_parser("run", help="Spawn agents for a task")
    p_spawn_run.add_argument("--task", "-t", required=True, help="Task description")
    p_spawn_run.add_argument("--count", "-n", type=int, default=1, help="Number of agents")
    p_spawn_run.add_argument("--model", default="llama3", help="Model to use")
    p_spawn_run.add_argument("--backend", default="ollama", help="Backend (ollama/claude/gemini)")
    spawn_sub.add_parser("list", help="List running agents")
    p_spawn_kill = spawn_sub.add_parser("kill", help="Kill an agent")
    p_spawn_kill.add_argument("name", help="Agent name to kill")

    # workflow
    p_wf = sub.add_parser("workflow", help="Manage DAG workflows")
    wf_sub = p_wf.add_subparsers(dest="workflow_command")
    p_wf_create = wf_sub.add_parser("create", help="Create workflow from JSON file")
    p_wf_create.add_argument("--file", "-f", required=True, help="Path to workflow JSON")
    p_wf_run = wf_sub.add_parser("run", help="Start a workflow run")
    p_wf_run.add_argument("id", help="Workflow ID")
    p_wf_run.add_argument("--input", "-i", default="", help="Input prompt")
    p_wf_status = wf_sub.add_parser("status", help="Show run status")
    p_wf_status.add_argument("run_id", help="Run ID")
    wf_sub.add_parser("list", help="List workflows")

    return parser


def _fix_windows_encoding() -> None:
    """Fix Windows console encoding: cp1252 cannot handle unicode output."""
    if sys.platform != "win32":
        return
    for stream_name in ("stdout", "stderr"):
        stream = getattr(sys, stream_name, None)
        if stream is None:
            continue
        # Only wrap if it has a raw buffer and is not already UTF-8
        buf = getattr(stream, "buffer", None)
        if buf is None:
            continue
        if getattr(stream, "encoding", "").lower().replace("-", "") == "utf8":
            continue
        try:
            wrapped = io.TextIOWrapper(buf, encoding="utf-8", errors="replace")
            setattr(sys, stream_name, wrapped)
        except Exception:
            pass


def main(argv: list[str] | None = None) -> int:
    """Entry point for the brynq-runtime CLI."""
    _fix_windows_encoding()

    parser = build_parser()
    args = parser.parse_args(argv)

    if not args.command:
        parser.print_help()
        return 0

    config = load_config()

    commands: dict[str, Any] = {
        "start": cmd_start,
        "stop": cmd_stop,
        "status": cmd_status,
        "models": cmd_models,
        "login": cmd_login,
        "chat": cmd_chat,
        "chain": cmd_chain,
        "batch": cmd_batch,
        "pipe": cmd_pipe,
        "watch": cmd_watch,
        "stats": cmd_stats,
        "insights": cmd_insights,
        "learn": cmd_learn,
        "preferences": cmd_preferences,
        "reset-learning": cmd_reset_learning,
        "trends": cmd_trends,
        "costs": cmd_costs,
        "ab": cmd_ab,
        "setup": cmd_setup,
        "completions": cmd_completions,
        "monitor": cmd_monitor,
        "dashboard": cmd_dashboard,
        "tui": cmd_tui,
        "spawn": cmd_spawn,
        "workflow": cmd_workflow,
        "connect": cmd_connect,
        "room": cmd_room,
        "profile": cmd_profile,
        "rate": cmd_rate_agent,
        "reviews": cmd_agent_reviews,
    }

    handler = commands.get(args.command)
    if handler is None:
        parser.print_help()
        return 1

    return handler(args, config)


if __name__ == "__main__":
    sys.exit(main())
