diff --git a/README.md b/README.md index 909b073d..129a57e6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

- Evolution Foundation + Evolution Foundation

@@ -61,12 +61,12 @@ It turns a single CLI installation into a team of **38 specialized agents** orga ## Screenshots

- Overview - Agents + Overview + Agents

- Integrations - Costs + Integrations + Costs

--- @@ -155,7 +155,7 @@ This downloads and runs the interactive setup wizard automatically. ### Method 2 — Manual clone ```bash -git clone https://github.com/EvolutionAPI/evo-nexus.git +git clone --depth 1 https://github.com/EvolutionAPI/evo-nexus.git cd evo-nexus # Interactive setup wizard — checks prerequisites, creates config files diff --git a/config/providers.example.json b/config/providers.example.json index 35d1df4f..5b4b6dfb 100644 --- a/config/providers.example.json +++ b/config/providers.example.json @@ -24,18 +24,18 @@ }, "openai": { "name": "OpenAI", - "description": "GPT-4o, GPT-4.1, o3 via OpenAI API", + "description": "GPT-4.x via API Key ou GPT-5.x via Codex OAuth", "cli_command": "openclaude", "env_vars": { "CLAUDE_CODE_USE_OPENAI": "1", "OPENAI_API_KEY": "", - "OPENAI_MODEL": "" + "OPENAI_MODEL": "gpt-5.4" }, "default_model": "gpt-4.1", "requires_logout": true }, "gemini": { - "name": "Google Gemini", + "name": "Google Gemini (em breve)", "description": "Gemini 2.5 Pro/Flash via Google AI", "cli_command": "openclaude", "env_vars": { @@ -44,21 +44,11 @@ "GEMINI_MODEL": "" }, "default_model": "gemini-2.5-pro", - "requires_logout": true - }, - "codex_auth": { - "name": "Codex Auth (OpenAI via OAuth)", - "description": "Usa autenticacao do Codex CLI para acessar modelos OpenAI", - "cli_command": "openclaude", - "env_vars": { - "CLAUDE_CODE_USE_OPENAI": "1", - "OPENAI_API_KEY": "" - }, "requires_logout": true, - "setup_hint": "Run 'codex login' first, then copy the API key" + "coming_soon": true }, "bedrock": { - "name": "AWS Bedrock", + "name": "AWS Bedrock (em breve)", "description": "Claude via AWS Bedrock", "cli_command": "openclaude", "env_vars": { @@ -66,10 +56,11 @@ "AWS_REGION": "", "AWS_BEARER_TOKEN_BEDROCK": "" }, - "requires_logout": true + "requires_logout": true, + "coming_soon": true }, "vertex": { - "name": "Google Vertex AI", + "name": "Google Vertex AI (em breve)", "description": "Claude via Google Cloud Vertex AI", "cli_command": "openclaude", "env_vars": { @@ -78,7 +69,8 @@ "CLOUD_ML_REGION": "" }, "default_region": "us-east5", - "requires_logout": true + "requires_logout": true, + "coming_soon": true } } } diff --git a/dashboard/backend/routes/config.py b/dashboard/backend/routes/config.py index c2763e90..a509374d 100644 --- a/dashboard/backend/routes/config.py +++ b/dashboard/backend/routes/config.py @@ -10,9 +10,20 @@ @bp.route("/api/config/workspace-status") def workspace_status(): - """Check if workspace.yaml exists (CLI setup was done).""" + """Check if workspace.yaml exists AND has owner configured.""" config_path = WORKSPACE / "config" / "workspace.yaml" - return jsonify({"configured": config_path.is_file()}) + if not config_path.is_file(): + return jsonify({"configured": False}) + # File exists but check if owner is actually filled in + try: + content = config_path.read_text(encoding="utf-8") + import yaml + data = yaml.safe_load(content) or {} + ws = data.get("workspace", data) + owner = (ws.get("owner") or ws.get("owner_name") or "").strip() + return jsonify({"configured": bool(owner)}) + except Exception: + return jsonify({"configured": False}) @bp.route("/api/config/claude-md") diff --git a/dashboard/backend/routes/providers.py b/dashboard/backend/routes/providers.py index 032088aa..e0f2e8c9 100644 --- a/dashboard/backend/routes/providers.py +++ b/dashboard/backend/routes/providers.py @@ -5,14 +5,19 @@ injected when spawning sessions. """ +import base64 +import hashlib import json import os import re +import secrets import shutil import subprocess +import time +import urllib.parse from pathlib import Path -from flask import Blueprint, jsonify, request +from flask import Blueprint, jsonify, redirect, request, session from flask_login import login_required from routes._helpers import WORKSPACE @@ -21,6 +26,11 @@ PROVIDERS_CONFIG = WORKSPACE / "config" / "providers.json" +OPENAI_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann" +OPENAI_AUTH_URL = "https://auth.openai.com/oauth/authorize" +OPENAI_TOKEN_URL = "https://auth.openai.com/oauth/token" +CODEX_AUTH_FILE = Path.home() / ".codex" / "auth.json" + # Allowlisted CLI commands — only these binaries can be spawned ALLOWED_CLI_COMMANDS = frozenset({"claude", "openclaude"}) @@ -117,6 +127,44 @@ def _sanitize_env_vars(env_vars: dict) -> dict: return safe +def _save_codex_auth(tokens: dict): + """Save tokens to ~/.codex/auth.json in the format OpenClaude/Codex expects. + + The correct format uses auth_mode + tokens object, NOT the old + openai-codex wrapper that OpenClaude doesn't recognize. + """ + import base64 as _b64 + + access_token = tokens["access_token"] + refresh_token = tokens.get("refresh_token", "") + id_token = tokens.get("id_token", access_token) + + # Extract chatgpt_account_id from the access token JWT + account_id = "" + try: + payload_b64 = access_token.split(".")[1] + # Add padding + payload_b64 += "=" * (4 - len(payload_b64) % 4) + payload = json.loads(_b64.urlsafe_b64decode(payload_b64)) + account_id = payload.get("https://api.openai.com/auth", {}).get("chatgpt_account_id", "") + except Exception: + pass + + auth_data = { + "auth_mode": "Chatgpt", + "tokens": { + "access_token": access_token, + "refresh_token": refresh_token, + "id_token": id_token, + "account_id": account_id, + }, + "last_refresh": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + } + + CODEX_AUTH_FILE.parent.mkdir(parents=True, exist_ok=True) + CODEX_AUTH_FILE.write_text(json.dumps(auth_data, indent=2), encoding="utf-8") + + # ── Endpoints ────────────────────────────────────────────── @@ -198,14 +246,15 @@ def get_active_provider(): @bp.route("/api/providers/active", methods=["POST"]) @login_required def set_active_provider(): - """Set the active provider.""" + """Set the active provider. Use provider_id='none' to disable all.""" data = request.get_json(silent=True) or {} provider_id = data.get("provider_id") - if not provider_id: + if provider_id is None: return jsonify({"error": "provider_id is required"}), 400 config = _read_config() - if provider_id not in config.get("providers", {}): + # Allow "none" to disable all providers + if provider_id != "none" and provider_id not in config.get("providers", {}): return jsonify({"error": f"Unknown provider: {provider_id}"}), 400 config["active_provider"] = provider_id @@ -305,3 +354,179 @@ def test_provider(provider_id): "cli": cli, "path": result["path"], }) + + +# ── OpenAI Auth Flow ────────────────────────────── + + +@bp.route("/api/providers/openai/auth-start", methods=["POST"]) +@login_required +def openai_auth_start(): + """Generate PKCE + authorize URL for Browser OAuth flow.""" + code_verifier = secrets.token_urlsafe(64) + digest = hashlib.sha256(code_verifier.encode()).digest() + code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode() + state = secrets.token_urlsafe(32) + + session["openai_code_verifier"] = code_verifier + session["openai_oauth_state"] = state + + params = { + "response_type": "code", + "client_id": OPENAI_CLIENT_ID, + "redirect_uri": "http://localhost:1455/auth/callback", + "scope": "openid profile email offline_access api.connectors.read api.connectors.invoke", + "code_challenge": code_challenge, + "code_challenge_method": "S256", + "state": state, + "id_token_add_organizations": "true", + "codex_cli_simplified_flow": "true", + } + url = f"{OPENAI_AUTH_URL}?{urllib.parse.urlencode(params)}" + return jsonify({"authorize_url": url}) + + +@bp.route("/api/providers/openai/auth-complete", methods=["POST"]) +@login_required +def openai_auth_complete(): + """Receive callback URL pasted by user, extract code, exchange for tokens.""" + import requests as http_req + + data = request.get_json(silent=True) or {} + callback_url = data.get("callback_url", "") + + parsed = urllib.parse.urlparse(callback_url) + params = urllib.parse.parse_qs(parsed.query) + code = params.get("code", [None])[0] + + if not code: + return jsonify({"error": "URL invalida - nao contem codigo de autorizacao"}), 400 + + code_verifier = session.pop("openai_code_verifier", None) + if not code_verifier: + return jsonify({"error": "Sessao expirada - inicie o login novamente"}), 400 + + session.pop("openai_oauth_state", None) + + resp = http_req.post(OPENAI_TOKEN_URL, data={ + "grant_type": "authorization_code", + "code": code, + "redirect_uri": "http://localhost:1455/auth/callback", + "client_id": OPENAI_CLIENT_ID, + "code_verifier": code_verifier, + }, timeout=30) + + if resp.status_code != 200: + return jsonify({"error": f"Falha na troca de token (HTTP {resp.status_code})"}), 400 + + _save_codex_auth(resp.json()) + + config = _read_config() + config["active_provider"] = "openai" + _write_config(config) + + return jsonify({"status": "ok", "message": "Autenticado com sucesso!"}) + + +@bp.route("/api/providers/openai/device-start", methods=["POST"]) +@login_required +def openai_device_start(): + """Start device auth flow.""" + import requests as http_req + + resp = http_req.post("https://auth.openai.com/deviceauth/usercode", json={ + "client_id": OPENAI_CLIENT_ID, + }, timeout=15) + + if resp.status_code != 200: + return jsonify({"error": "Device auth nao disponivel para sua organizacao"}), 400 + + data = resp.json() + session["openai_device_auth_id"] = data["device_auth_id"] + session["openai_device_user_code"] = data["user_code"] + + return jsonify({ + "user_code": data["user_code"], + "verification_url": "https://auth.openai.com/codex/device", + "interval": data.get("interval", 5), + "expires_in": data.get("expires_in", 900), + }) + + +@bp.route("/api/providers/openai/device-poll", methods=["POST"]) +@login_required +def openai_device_poll(): + """Poll for device auth authorization.""" + import requests as http_req + + device_auth_id = session.get("openai_device_auth_id") + user_code = session.get("openai_device_user_code") + if not device_auth_id: + return jsonify({"status": "error", "message": "Nenhum login pendente"}), 400 + + resp = http_req.post("https://auth.openai.com/deviceauth/token", json={ + "device_auth_id": device_auth_id, + "user_code": user_code, + }, timeout=15) + + if resp.status_code in (403, 404): + return jsonify({"status": "pending"}) + + if resp.status_code != 200: + return jsonify({"status": "error", "message": "Polling falhou"}), 500 + + auth_data = resp.json() + + token_resp = http_req.post(OPENAI_TOKEN_URL, data={ + "grant_type": "authorization_code", + "code": auth_data["authorization_code"], + "code_verifier": auth_data["code_verifier"], + "client_id": OPENAI_CLIENT_ID, + }, timeout=15) + + if token_resp.status_code != 200: + return jsonify({"status": "error", "message": "Token exchange falhou"}), 500 + + _save_codex_auth(token_resp.json()) + + config = _read_config() + config["active_provider"] = "openai" + _write_config(config) + + session.pop("openai_device_auth_id", None) + session.pop("openai_device_user_code", None) + + return jsonify({"status": "authorized"}) + + +@bp.route("/api/providers/openai/status") +@login_required +def openai_status(): + """Check if Codex OAuth token exists and is valid.""" + if not CODEX_AUTH_FILE.is_file(): + return jsonify({"authenticated": False, "method": "none"}) + + try: + auth = json.loads(CODEX_AUTH_FILE.read_text(encoding="utf-8")) + # Support both new format (auth_mode+tokens) and old format (openai-codex) + tokens = auth.get("tokens", {}) + has_access = bool(tokens.get("access_token") or auth.get("openai-codex", {}).get("access")) + return jsonify({ + "authenticated": has_access, + "method": "codex_oauth", + "auth_mode": auth.get("auth_mode", "unknown"), + }) + except (json.JSONDecodeError, OSError): + return jsonify({"authenticated": False, "method": "none"}) + + +@bp.route("/api/providers/openai/logout", methods=["POST"]) +@login_required +def openai_logout(): + """Remove Codex auth.json and reset provider.""" + if CODEX_AUTH_FILE.is_file(): + CODEX_AUTH_FILE.unlink() + config = _read_config() + config["active_provider"] = "anthropic" + _write_config(config) + return jsonify({"status": "ok"}) diff --git a/dashboard/frontend/public/avatar/avatar_apex.png b/dashboard/frontend/public/avatar/avatar_apex.png deleted file mode 100644 index eb4b6e73..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_apex.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_apex.webp b/dashboard/frontend/public/avatar/avatar_apex.webp new file mode 100644 index 00000000..397129b9 Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_apex.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_aria.png b/dashboard/frontend/public/avatar/avatar_aria.png deleted file mode 100644 index fda06e86..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_aria.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_aria.webp b/dashboard/frontend/public/avatar/avatar_aria.webp new file mode 100644 index 00000000..fc14ff81 Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_aria.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_atlas.png b/dashboard/frontend/public/avatar/avatar_atlas.png deleted file mode 100644 index d25af31d..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_atlas.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_atlas.webp b/dashboard/frontend/public/avatar/avatar_atlas.webp new file mode 100644 index 00000000..c1779194 Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_atlas.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_bolt.png b/dashboard/frontend/public/avatar/avatar_bolt.png deleted file mode 100644 index ecbd700a..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_bolt.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_bolt.webp b/dashboard/frontend/public/avatar/avatar_bolt.webp new file mode 100644 index 00000000..3b9368fc Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_bolt.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_canvas.png b/dashboard/frontend/public/avatar/avatar_canvas.png deleted file mode 100644 index fd21d766..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_canvas.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_canvas.webp b/dashboard/frontend/public/avatar/avatar_canvas.webp new file mode 100644 index 00000000..18cbb842 Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_canvas.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_clawdia.png b/dashboard/frontend/public/avatar/avatar_clawdia.png deleted file mode 100644 index 3bfcf7d7..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_clawdia.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_clawdia.webp b/dashboard/frontend/public/avatar/avatar_clawdia.webp new file mode 100644 index 00000000..94911ddd Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_clawdia.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_compass.png b/dashboard/frontend/public/avatar/avatar_compass.png deleted file mode 100644 index 3dbd06e1..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_compass.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_compass.webp b/dashboard/frontend/public/avatar/avatar_compass.webp new file mode 100644 index 00000000..e2a1ca63 Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_compass.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_dex.png b/dashboard/frontend/public/avatar/avatar_dex.png deleted file mode 100644 index d4215e7b..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_dex.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_dex.webp b/dashboard/frontend/public/avatar/avatar_dex.webp new file mode 100644 index 00000000..05bd6268 Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_dex.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_echo.png b/dashboard/frontend/public/avatar/avatar_echo.png deleted file mode 100644 index 61132543..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_echo.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_echo.webp b/dashboard/frontend/public/avatar/avatar_echo.webp new file mode 100644 index 00000000..aea66f1e Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_echo.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_flow.png b/dashboard/frontend/public/avatar/avatar_flow.png deleted file mode 100644 index 3d6afe7d..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_flow.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_flow.webp b/dashboard/frontend/public/avatar/avatar_flow.webp new file mode 100644 index 00000000..ffa5365a Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_flow.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_flux.png b/dashboard/frontend/public/avatar/avatar_flux.png deleted file mode 100644 index 4d04ca12..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_flux.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_flux.webp b/dashboard/frontend/public/avatar/avatar_flux.webp new file mode 100644 index 00000000..c7260fe0 Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_flux.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_grid.png b/dashboard/frontend/public/avatar/avatar_grid.png deleted file mode 100644 index b4b2ec14..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_grid.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_grid.webp b/dashboard/frontend/public/avatar/avatar_grid.webp new file mode 100644 index 00000000..6ed1ff54 Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_grid.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_hawk.png b/dashboard/frontend/public/avatar/avatar_hawk.png deleted file mode 100644 index bb05d5ab..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_hawk.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_hawk.webp b/dashboard/frontend/public/avatar/avatar_hawk.webp new file mode 100644 index 00000000..ee0db0f9 Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_hawk.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_helm.png b/dashboard/frontend/public/avatar/avatar_helm.png deleted file mode 100644 index 63a67e76..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_helm.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_helm.webp b/dashboard/frontend/public/avatar/avatar_helm.webp new file mode 100644 index 00000000..743f6b62 Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_helm.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_kai.png b/dashboard/frontend/public/avatar/avatar_kai.png deleted file mode 100644 index e00106d6..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_kai.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_kai.webp b/dashboard/frontend/public/avatar/avatar_kai.webp new file mode 100644 index 00000000..fcda6f2d Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_kai.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_lens.png b/dashboard/frontend/public/avatar/avatar_lens.png deleted file mode 100644 index b0b774c4..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_lens.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_lens.webp b/dashboard/frontend/public/avatar/avatar_lens.webp new file mode 100644 index 00000000..52c59391 Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_lens.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_lex.png b/dashboard/frontend/public/avatar/avatar_lex.png deleted file mode 100644 index 4d7d91dd..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_lex.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_lex.webp b/dashboard/frontend/public/avatar/avatar_lex.webp new file mode 100644 index 00000000..f126ce28 Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_lex.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_lumen.png b/dashboard/frontend/public/avatar/avatar_lumen.png deleted file mode 100644 index c8b5d763..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_lumen.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_lumen.webp b/dashboard/frontend/public/avatar/avatar_lumen.webp new file mode 100644 index 00000000..85af3462 Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_lumen.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_mako.png b/dashboard/frontend/public/avatar/avatar_mako.png deleted file mode 100644 index 46d09817..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_mako.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_mako.webp b/dashboard/frontend/public/avatar/avatar_mako.webp new file mode 100644 index 00000000..a01fa584 Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_mako.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_mentor.png b/dashboard/frontend/public/avatar/avatar_mentor.png deleted file mode 100644 index 1d39e16d..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_mentor.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_mentor.webp b/dashboard/frontend/public/avatar/avatar_mentor.webp new file mode 100644 index 00000000..dfeac821 Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_mentor.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_mirror.png b/dashboard/frontend/public/avatar/avatar_mirror.png deleted file mode 100644 index 6e76f3cc..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_mirror.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_mirror.webp b/dashboard/frontend/public/avatar/avatar_mirror.webp new file mode 100644 index 00000000..74f394e6 Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_mirror.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_nex.png b/dashboard/frontend/public/avatar/avatar_nex.png deleted file mode 100644 index 08fe6cc0..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_nex.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_nex.webp b/dashboard/frontend/public/avatar/avatar_nex.webp new file mode 100644 index 00000000..42acf5cf Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_nex.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_nova.png b/dashboard/frontend/public/avatar/avatar_nova.png deleted file mode 100644 index db4de361..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_nova.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_nova.webp b/dashboard/frontend/public/avatar/avatar_nova.webp new file mode 100644 index 00000000..382b8d3f Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_nova.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_oath.png b/dashboard/frontend/public/avatar/avatar_oath.png deleted file mode 100644 index d0302429..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_oath.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_oath.webp b/dashboard/frontend/public/avatar/avatar_oath.webp new file mode 100644 index 00000000..c8a1607c Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_oath.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_oracle.png b/dashboard/frontend/public/avatar/avatar_oracle.png deleted file mode 100644 index ca2adc22..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_oracle.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_oracle.webp b/dashboard/frontend/public/avatar/avatar_oracle.webp new file mode 100644 index 00000000..0d0abff9 Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_oracle.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_pixel.png b/dashboard/frontend/public/avatar/avatar_pixel.png deleted file mode 100644 index 87b32aeb..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_pixel.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_pixel.webp b/dashboard/frontend/public/avatar/avatar_pixel.webp new file mode 100644 index 00000000..cb5c5ea1 Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_pixel.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_prism.png b/dashboard/frontend/public/avatar/avatar_prism.png deleted file mode 100644 index f72c6c2d..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_prism.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_prism.webp b/dashboard/frontend/public/avatar/avatar_prism.webp new file mode 100644 index 00000000..f59538b5 Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_prism.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_probe.png b/dashboard/frontend/public/avatar/avatar_probe.png deleted file mode 100644 index 34bf5fe3..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_probe.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_probe.webp b/dashboard/frontend/public/avatar/avatar_probe.webp new file mode 100644 index 00000000..da0b66e0 Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_probe.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_pulse.png b/dashboard/frontend/public/avatar/avatar_pulse.png deleted file mode 100644 index 6ccbdf17..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_pulse.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_pulse.webp b/dashboard/frontend/public/avatar/avatar_pulse.webp new file mode 100644 index 00000000..6b8d8a9c Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_pulse.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_quill.png b/dashboard/frontend/public/avatar/avatar_quill.png deleted file mode 100644 index 9e21f3e1..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_quill.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_quill.webp b/dashboard/frontend/public/avatar/avatar_quill.webp new file mode 100644 index 00000000..d958d7d9 Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_quill.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_raven.png b/dashboard/frontend/public/avatar/avatar_raven.png deleted file mode 100644 index 783bcba7..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_raven.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_raven.webp b/dashboard/frontend/public/avatar/avatar_raven.webp new file mode 100644 index 00000000..5db5da06 Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_raven.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_sage.png b/dashboard/frontend/public/avatar/avatar_sage.png deleted file mode 100644 index 34fd4ec0..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_sage.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_sage.webp b/dashboard/frontend/public/avatar/avatar_sage.webp new file mode 100644 index 00000000..06f4f076 Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_sage.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_scout.png b/dashboard/frontend/public/avatar/avatar_scout.png deleted file mode 100644 index 36db6de4..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_scout.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_scout.webp b/dashboard/frontend/public/avatar/avatar_scout.webp new file mode 100644 index 00000000..71b9d088 Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_scout.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_scroll.png b/dashboard/frontend/public/avatar/avatar_scroll.png deleted file mode 100644 index 696f7d04..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_scroll.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_scroll.webp b/dashboard/frontend/public/avatar/avatar_scroll.webp new file mode 100644 index 00000000..dab9d9b8 Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_scroll.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_trail.png b/dashboard/frontend/public/avatar/avatar_trail.png deleted file mode 100644 index b787dc59..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_trail.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_trail.webp b/dashboard/frontend/public/avatar/avatar_trail.webp new file mode 100644 index 00000000..dd8b496d Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_trail.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_vault.png b/dashboard/frontend/public/avatar/avatar_vault.png deleted file mode 100644 index c9e22f06..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_vault.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_vault.webp b/dashboard/frontend/public/avatar/avatar_vault.webp new file mode 100644 index 00000000..21c77cd7 Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_vault.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_zara.png b/dashboard/frontend/public/avatar/avatar_zara.png deleted file mode 100644 index 4db7c896..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_zara.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_zara.webp b/dashboard/frontend/public/avatar/avatar_zara.webp new file mode 100644 index 00000000..90950f23 Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_zara.webp differ diff --git a/dashboard/frontend/public/avatar/avatar_zen.png b/dashboard/frontend/public/avatar/avatar_zen.png deleted file mode 100644 index 0f43a4a6..00000000 Binary files a/dashboard/frontend/public/avatar/avatar_zen.png and /dev/null differ diff --git a/dashboard/frontend/public/avatar/avatar_zen.webp b/dashboard/frontend/public/avatar/avatar_zen.webp new file mode 100644 index 00000000..8e54ebe2 Binary files /dev/null and b/dashboard/frontend/public/avatar/avatar_zen.webp differ diff --git a/dashboard/frontend/src/assets/hero.png b/dashboard/frontend/src/assets/hero.png deleted file mode 100644 index cc51a3d2..00000000 Binary files a/dashboard/frontend/src/assets/hero.png and /dev/null differ diff --git a/dashboard/frontend/src/assets/hero.webp b/dashboard/frontend/src/assets/hero.webp new file mode 100644 index 00000000..ac29b981 Binary files /dev/null and b/dashboard/frontend/src/assets/hero.webp differ diff --git a/dashboard/frontend/src/components/AgentTerminal.tsx b/dashboard/frontend/src/components/AgentTerminal.tsx index 9e0c4c5f..872b805d 100644 --- a/dashboard/frontend/src/components/AgentTerminal.tsx +++ b/dashboard/frontend/src/components/AgentTerminal.tsx @@ -12,11 +12,11 @@ interface AgentTerminalProps { const CC_WEB_HTTP = import.meta.env.DEV ? 'http://localhost:32352' - : `${window.location.protocol}//${window.location.hostname}:32352` + : `${window.location.origin}/terminal` const CC_WEB_WS = import.meta.env.DEV ? 'ws://localhost:32352' - : `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.hostname}:32352` + : `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/terminal` type Status = 'connecting' | 'ready' | 'starting' | 'running' | 'error' | 'exited' diff --git a/dashboard/frontend/src/lib/agent-meta.ts b/dashboard/frontend/src/lib/agent-meta.ts index b64a161b..733cf0b7 100644 --- a/dashboard/frontend/src/lib/agent-meta.ts +++ b/dashboard/frontend/src/lib/agent-meta.ts @@ -31,44 +31,44 @@ export interface AgentMeta { } const AGENT_META: Record = { - 'atlas-project': { icon: FolderKanban, color: '#60A5FA', command: '/atlas-project', label: 'Projects', avatar: '/avatar/avatar_atlas.png' }, - 'clawdia-assistant': { icon: Brain, color: '#22D3EE', command: '/clawdia', label: 'Operations', avatar: '/avatar/avatar_clawdia.png' }, - 'flux-finance': { icon: DollarSign, color: '#34D399', command: '/flux', label: 'Finance', avatar: '/avatar/avatar_flux.png' }, - 'kai-personal-assistant': { icon: Heart, color: '#F472B6', command: '/kai', label: 'Personal', avatar: '/avatar/avatar_kai.png' }, - 'mentor-courses': { icon: GraduationCap, color: '#FBBF24', command: '/mentor', label: 'Courses', avatar: '/avatar/avatar_mentor.png' }, - 'lumen-learning': { icon: Zap, color: '#FCD34D', command: '/lumen-learning', label: 'Learning Retention', avatar: '/avatar/avatar_lumen.png' }, - 'nex-sales': { icon: Target, color: '#FB923C', command: '/nex', label: 'Sales', avatar: '/avatar/avatar_nex.png' }, - 'pixel-social-media': { icon: Camera, color: '#A78BFA', command: '/pixel', label: 'Social Media', avatar: '/avatar/avatar_pixel.png' }, - 'pulse-community': { icon: Users, color: '#2DD4BF', command: '/pulse', label: 'Community', avatar: '/avatar/avatar_pulse.png' }, - 'sage-strategy': { icon: Compass, color: '#818CF8', command: '/sage', label: 'Strategy', avatar: '/avatar/avatar_sage.png' }, - oracle: { icon: BookOpen, color: '#F59E0B', command: '/oracle', label: 'Knowledge', avatar: '/avatar/avatar_oracle.png' }, - 'mako-marketing': { icon: Megaphone, color: '#FB923C', command: '/mako', label: 'Marketing', avatar: '/avatar/avatar_mako.png' }, - 'aria-hr': { icon: UserCheck, color: '#F472B6', command: '/aria', label: 'HR / People', avatar: '/avatar/avatar_aria.png' }, - 'zara-cs': { icon: Headphones, color: '#22D3EE', command: '/zara', label: 'Customer Success', avatar: '/avatar/avatar_zara.png' }, - 'lex-legal': { icon: Scale, color: '#C084FC', command: '/lex', label: 'Legal', avatar: '/avatar/avatar_lex.png' }, - 'nova-product': { icon: Lightbulb, color: '#60A5FA', command: '/nova', label: 'Product', avatar: '/avatar/avatar_nova.png' }, - 'dex-data': { icon: BarChart3, color: '#FBBF24', command: '/dex', label: 'Data / BI', avatar: '/avatar/avatar_dex.png' }, - 'helm-conductor': { icon: Navigation, color: '#14B8A6', command: '/helm-conductor', label: 'Cycle Orchestration', avatar: '/avatar/avatar_helm.png' }, - 'mirror-retro': { icon: History, color: '#94A3B8', command: '/mirror-retro', label: 'Retrospective', avatar: '/avatar/avatar_mirror.png' }, - 'apex-architect': { icon: Bot, color: '#A78BFA', command: '/apex-architect', label: 'Architect', avatar: '/avatar/avatar_apex.png' }, - 'bolt-executor': { icon: Bot, color: '#FCD34D', command: '/bolt-executor', label: 'Executor', avatar: '/avatar/avatar_bolt.png' }, - 'canvas-designer': { icon: Bot, color: '#F472B6', command: '/canvas-designer', label: 'Designer', avatar: '/avatar/avatar_canvas.png' }, - 'compass-planner': { icon: Bot, color: '#60A5FA', command: '/compass-planner', label: 'Planner', avatar: '/avatar/avatar_compass.png' }, - 'echo-analyst': { icon: Bot, color: '#22D3EE', command: '/echo-analyst', label: 'Analyst', avatar: '/avatar/avatar_echo.png' }, - 'flow-git': { icon: Bot, color: '#34D399', command: '/flow-git', label: 'Git Master', avatar: '/avatar/avatar_flow.png' }, - 'grid-tester': { icon: Bot, color: '#FBBF24', command: '/grid-tester', label: 'Test Engineer', avatar: '/avatar/avatar_grid.png' }, - 'hawk-debugger': { icon: Bot, color: '#FB923C', command: '/hawk-debugger', label: 'Debugger', avatar: '/avatar/avatar_hawk.png' }, - 'lens-reviewer': { icon: Bot, color: '#C084FC', command: '/lens-reviewer', label: 'Code Reviewer', avatar: '/avatar/avatar_lens.png' }, - 'oath-verifier': { icon: Bot, color: '#2DD4BF', command: '/oath-verifier', label: 'Verifier', avatar: '/avatar/avatar_oath.png' }, - 'prism-scientist': { icon: Bot, color: '#818CF8', command: '/prism-scientist', label: 'Scientist', avatar: '/avatar/avatar_prism.png' }, - 'probe-qa': { icon: Bot, color: '#F59E0B', command: '/probe-qa', label: 'QA Tester', avatar: '/avatar/avatar_probe.png' }, - 'quill-writer': { icon: Bot, color: '#94A3B8', command: '/quill-writer', label: 'Writer', avatar: '/avatar/avatar_quill.png' }, - 'raven-critic': { icon: Bot, color: '#F87171', command: '/raven-critic', label: 'Critic', avatar: '/avatar/avatar_raven.png' }, - 'scout-explorer': { icon: Bot, color: '#22D3EE', command: '/scout-explorer', label: 'Explorer', avatar: '/avatar/avatar_scout.png' }, - 'scroll-docs': { icon: Bot, color: '#FCD34D', command: '/scroll-docs', label: 'Document Specialist', avatar: '/avatar/avatar_scroll.png' }, - 'trail-tracer': { icon: Bot, color: '#34D399', command: '/trail-tracer', label: 'Tracer', avatar: '/avatar/avatar_trail.png' }, - 'vault-security': { icon: Bot, color: '#F87171', command: '/vault-security', label: 'Security Reviewer', avatar: '/avatar/avatar_vault.png' }, - 'zen-simplifier': { icon: Bot, color: '#A78BFA', command: '/zen-simplifier', label: 'Code Simplifier', avatar: '/avatar/avatar_zen.png' }, + 'atlas-project': { icon: FolderKanban, color: '#60A5FA', command: '/atlas-project', label: 'Projects', avatar: '/avatar/avatar_atlas.webp' }, + 'clawdia-assistant': { icon: Brain, color: '#22D3EE', command: '/clawdia', label: 'Operations', avatar: '/avatar/avatar_clawdia.webp' }, + 'flux-finance': { icon: DollarSign, color: '#34D399', command: '/flux', label: 'Finance', avatar: '/avatar/avatar_flux.webp' }, + 'kai-personal-assistant': { icon: Heart, color: '#F472B6', command: '/kai', label: 'Personal', avatar: '/avatar/avatar_kai.webp' }, + 'mentor-courses': { icon: GraduationCap, color: '#FBBF24', command: '/mentor', label: 'Courses', avatar: '/avatar/avatar_mentor.webp' }, + 'lumen-learning': { icon: Zap, color: '#FCD34D', command: '/lumen-learning', label: 'Learning Retention', avatar: '/avatar/avatar_lumen.webp' }, + 'nex-sales': { icon: Target, color: '#FB923C', command: '/nex', label: 'Sales', avatar: '/avatar/avatar_nex.webp' }, + 'pixel-social-media': { icon: Camera, color: '#A78BFA', command: '/pixel', label: 'Social Media', avatar: '/avatar/avatar_pixel.webp' }, + 'pulse-community': { icon: Users, color: '#2DD4BF', command: '/pulse', label: 'Community', avatar: '/avatar/avatar_pulse.webp' }, + 'sage-strategy': { icon: Compass, color: '#818CF8', command: '/sage', label: 'Strategy', avatar: '/avatar/avatar_sage.webp' }, + oracle: { icon: BookOpen, color: '#F59E0B', command: '/oracle', label: 'Knowledge', avatar: '/avatar/avatar_oracle.webp' }, + 'mako-marketing': { icon: Megaphone, color: '#FB923C', command: '/mako', label: 'Marketing', avatar: '/avatar/avatar_mako.webp' }, + 'aria-hr': { icon: UserCheck, color: '#F472B6', command: '/aria', label: 'HR / People', avatar: '/avatar/avatar_aria.webp' }, + 'zara-cs': { icon: Headphones, color: '#22D3EE', command: '/zara', label: 'Customer Success', avatar: '/avatar/avatar_zara.webp' }, + 'lex-legal': { icon: Scale, color: '#C084FC', command: '/lex', label: 'Legal', avatar: '/avatar/avatar_lex.webp' }, + 'nova-product': { icon: Lightbulb, color: '#60A5FA', command: '/nova', label: 'Product', avatar: '/avatar/avatar_nova.webp' }, + 'dex-data': { icon: BarChart3, color: '#FBBF24', command: '/dex', label: 'Data / BI', avatar: '/avatar/avatar_dex.webp' }, + 'helm-conductor': { icon: Navigation, color: '#14B8A6', command: '/helm-conductor', label: 'Cycle Orchestration', avatar: '/avatar/avatar_helm.webp' }, + 'mirror-retro': { icon: History, color: '#94A3B8', command: '/mirror-retro', label: 'Retrospective', avatar: '/avatar/avatar_mirror.webp' }, + 'apex-architect': { icon: Bot, color: '#A78BFA', command: '/apex-architect', label: 'Architect', avatar: '/avatar/avatar_apex.webp' }, + 'bolt-executor': { icon: Bot, color: '#FCD34D', command: '/bolt-executor', label: 'Executor', avatar: '/avatar/avatar_bolt.webp' }, + 'canvas-designer': { icon: Bot, color: '#F472B6', command: '/canvas-designer', label: 'Designer', avatar: '/avatar/avatar_canvas.webp' }, + 'compass-planner': { icon: Bot, color: '#60A5FA', command: '/compass-planner', label: 'Planner', avatar: '/avatar/avatar_compass.webp' }, + 'echo-analyst': { icon: Bot, color: '#22D3EE', command: '/echo-analyst', label: 'Analyst', avatar: '/avatar/avatar_echo.webp' }, + 'flow-git': { icon: Bot, color: '#34D399', command: '/flow-git', label: 'Git Master', avatar: '/avatar/avatar_flow.webp' }, + 'grid-tester': { icon: Bot, color: '#FBBF24', command: '/grid-tester', label: 'Test Engineer', avatar: '/avatar/avatar_grid.webp' }, + 'hawk-debugger': { icon: Bot, color: '#FB923C', command: '/hawk-debugger', label: 'Debugger', avatar: '/avatar/avatar_hawk.webp' }, + 'lens-reviewer': { icon: Bot, color: '#C084FC', command: '/lens-reviewer', label: 'Code Reviewer', avatar: '/avatar/avatar_lens.webp' }, + 'oath-verifier': { icon: Bot, color: '#2DD4BF', command: '/oath-verifier', label: 'Verifier', avatar: '/avatar/avatar_oath.webp' }, + 'prism-scientist': { icon: Bot, color: '#818CF8', command: '/prism-scientist', label: 'Scientist', avatar: '/avatar/avatar_prism.webp' }, + 'probe-qa': { icon: Bot, color: '#F59E0B', command: '/probe-qa', label: 'QA Tester', avatar: '/avatar/avatar_probe.webp' }, + 'quill-writer': { icon: Bot, color: '#94A3B8', command: '/quill-writer', label: 'Writer', avatar: '/avatar/avatar_quill.webp' }, + 'raven-critic': { icon: Bot, color: '#F87171', command: '/raven-critic', label: 'Critic', avatar: '/avatar/avatar_raven.webp' }, + 'scout-explorer': { icon: Bot, color: '#22D3EE', command: '/scout-explorer', label: 'Explorer', avatar: '/avatar/avatar_scout.webp' }, + 'scroll-docs': { icon: Bot, color: '#FCD34D', command: '/scroll-docs', label: 'Document Specialist', avatar: '/avatar/avatar_scroll.webp' }, + 'trail-tracer': { icon: Bot, color: '#34D399', command: '/trail-tracer', label: 'Tracer', avatar: '/avatar/avatar_trail.webp' }, + 'vault-security': { icon: Bot, color: '#F87171', command: '/vault-security', label: 'Security Reviewer', avatar: '/avatar/avatar_vault.webp' }, + 'zen-simplifier': { icon: Bot, color: '#A78BFA', command: '/zen-simplifier', label: 'Code Simplifier', avatar: '/avatar/avatar_zen.webp' }, } const DEFAULT_META: AgentMeta = { diff --git a/dashboard/frontend/src/pages/Login.tsx b/dashboard/frontend/src/pages/Login.tsx index cd7f91f2..ac7d7100 100644 --- a/dashboard/frontend/src/pages/Login.tsx +++ b/dashboard/frontend/src/pages/Login.tsx @@ -1,6 +1,81 @@ -import { useState, type FormEvent } from 'react' +import { useState, useEffect, useRef, type FormEvent } from 'react' import { useAuth } from '../context/AuthContext' +/* ── Animated mesh background ── */ +function NetworkCanvas() { + const ref = useRef(null) + + useEffect(() => { + const canvas = ref.current + if (!canvas) return + const ctx = canvas.getContext('2d') + if (!ctx) return + + let animId: number + let particles: { x: number; y: number; vx: number; vy: number }[] = [] + + const resize = () => { + canvas.width = window.innerWidth + canvas.height = window.innerHeight + } + + const init = () => { + resize() + const count = Math.floor((canvas.width * canvas.height) / 18000) + particles = Array.from({ length: Math.min(count, 80) }, () => ({ + x: Math.random() * canvas.width, + y: Math.random() * canvas.height, + vx: (Math.random() - 0.5) * 0.3, + vy: (Math.random() - 0.5) * 0.3, + })) + } + + const draw = () => { + ctx.clearRect(0, 0, canvas.width, canvas.height) + const maxDist = 150 + + for (let i = 0; i < particles.length; i++) { + const p = particles[i] + p.x += p.vx + p.y += p.vy + if (p.x < 0 || p.x > canvas.width) p.vx *= -1 + if (p.y < 0 || p.y > canvas.height) p.vy *= -1 + + ctx.beginPath() + ctx.arc(p.x, p.y, 1.5, 0, Math.PI * 2) + ctx.fillStyle = 'rgba(0, 255, 167, 0.25)' + ctx.fill() + + for (let j = i + 1; j < particles.length; j++) { + const q = particles[j] + const dx = p.x - q.x + const dy = p.y - q.y + const dist = Math.sqrt(dx * dx + dy * dy) + if (dist < maxDist) { + ctx.beginPath() + ctx.moveTo(p.x, p.y) + ctx.lineTo(q.x, q.y) + ctx.strokeStyle = `rgba(0, 255, 167, ${0.06 * (1 - dist / maxDist)})` + ctx.lineWidth = 0.5 + ctx.stroke() + } + } + } + animId = requestAnimationFrame(draw) + } + + init() + draw() + window.addEventListener('resize', init) + return () => { + cancelAnimationFrame(animId) + window.removeEventListener('resize', init) + } + }, []) + + return +} + export default function Login() { const { login } = useAuth() const [username, setUsername] = useState('') @@ -15,7 +90,6 @@ export default function Login() { setError('Username and password are required') return } - setSubmitting(true) try { await login(username.trim(), password) @@ -26,65 +100,73 @@ export default function Login() { } } + const inp = "w-full px-4 py-3 rounded-lg bg-[#0f1520] border border-[#1e2a3a] text-[#e2e8f0] placeholder-[#3d4f65] text-sm transition-colors duration-200 focus:outline-none focus:border-[#00FFA7]/60 focus:ring-1 focus:ring-[#00FFA7]/20" + const lbl = "block text-[11px] font-semibold text-[#5a6b7f] mb-1.5 tracking-[0.08em] uppercase" + return ( -
-
-
- {/* Logo */} -
-

- Evo - Nexus -

-

Sign in to your dashboard

-
+
+ - {error && ( -
- {error} -
- )} - -
-
- - setUsername(e.target.value)} - className="w-full px-4 py-2.5 rounded-lg bg-[#0C111D] border border-[#344054] text-white placeholder-[#667085] focus:outline-none focus:border-[#00FFA7] focus:ring-1 focus:ring-[#00FFA7] transition-colors text-sm" - placeholder="Username" - autoFocus - /> -
+
+
-
- - setPassword(e.target.value)} - className="w-full px-4 py-2.5 rounded-lg bg-[#0C111D] border border-[#344054] text-white placeholder-[#667085] focus:outline-none focus:border-[#00FFA7] focus:ring-1 focus:ring-[#00FFA7] transition-colors text-sm" - placeholder="Password" - /> + {/* Header */} +
+
+
+ + + + + +
+
+

+ EvoNexus +

+

Sign in to continue

+
+
+ + {/* Form */} +
+ {error && ( +
+ {error} +
+ )} - - +
+
+ + setUsername(e.target.value)} + className={inp} placeholder="Username" autoFocus autoComplete="username" /> +
+
+ + setPassword(e.target.value)} + className={inp} placeholder="Password" autoComplete="current-password" /> +
+ + +
+
- {/* Credits */} - +

) diff --git a/dashboard/frontend/src/pages/Overview.tsx b/dashboard/frontend/src/pages/Overview.tsx index 0a19731f..634a1fdc 100644 --- a/dashboard/frontend/src/pages/Overview.tsx +++ b/dashboard/frontend/src/pages/Overview.tsx @@ -11,10 +11,9 @@ import { TrendingUp, TrendingDown, Minus, - MessageSquare, BarChart3, GitBranch, - Sun, + Settings, type LucideIcon, } from 'lucide-react' import { api } from '../lib/api' @@ -203,8 +202,8 @@ function ActiveAgentsBar({ agents, loading }: { agents: ActiveAgent[]; loading: // --- Quick Actions --- const QUICK_ACTIONS = [ - { label: 'Run Morning Briefing', icon: Sun, to: '/chat', hint: 'Start your day' }, - { label: 'Open Chat', icon: MessageSquare, to: '/chat', hint: 'Talk to agents' }, + { label: 'Agents', icon: Bot, to: '/agents', hint: 'Talk to agents' }, + { label: 'Providers', icon: Settings, to: '/providers', hint: 'AI configuration' }, { label: 'View Costs', icon: BarChart3, to: '/costs', hint: 'Financial overview' }, { label: 'Check GitHub', icon: GitBranch, to: '/integrations', hint: 'Repo status' }, ] diff --git a/dashboard/frontend/src/pages/Providers.tsx b/dashboard/frontend/src/pages/Providers.tsx index 6488c222..74bf2173 100644 --- a/dashboard/frontend/src/pages/Providers.tsx +++ b/dashboard/frontend/src/pages/Providers.tsx @@ -1,88 +1,62 @@ import { useEffect, useState } from 'react' -import { - CheckCircle2, - AlertCircle, - Download, - Star, - RefreshCw, - Settings2, - X, - Zap, - TestTube2, -} from 'lucide-react' +import { CheckCircle2, AlertCircle, RefreshCw, X } from 'lucide-react' import { api } from '../lib/api' -interface ProviderEnvVars { - [key: string]: string -} +interface ProviderEnvVars { [key: string]: string } interface Provider { - id: string - name: string - description: string - cli_command: string - is_active: boolean - installed: boolean - version: string | null - path: string | null - has_config: boolean - env_vars: ProviderEnvVars - requires_logout: boolean - setup_hint: string | null - default_model: string | null - default_base_url: string | null - default_region: string | null + id: string; name: string; description: string; cli_command: string + is_active: boolean; installed: boolean; version: string | null; path: string | null + has_config: boolean; env_vars: ProviderEnvVars; requires_logout: boolean + setup_hint: string | null; default_model: string | null + default_base_url: string | null; default_region: string | null } interface ProvidersResponse { - providers: Provider[] - active_provider: string - claude_installed: boolean - openclaude_installed: boolean + providers: Provider[]; active_provider: string + claude_installed: boolean; openclaude_installed: boolean } -// Env var display names const ENV_VAR_LABELS: Record = { - CLAUDE_CODE_USE_OPENAI: 'Use OpenAI (flag)', - CLAUDE_CODE_USE_GEMINI: 'Use Gemini (flag)', - CLAUDE_CODE_USE_BEDROCK: 'Use Bedrock (flag)', - CLAUDE_CODE_USE_VERTEX: 'Use Vertex (flag)', - OPENAI_BASE_URL: 'Base URL', - OPENAI_API_KEY: 'API Key', - OPENAI_MODEL: 'Model', - GEMINI_API_KEY: 'API Key', - GEMINI_MODEL: 'Model', - AWS_REGION: 'AWS Region', - AWS_BEARER_TOKEN_BEDROCK: 'Bearer Token', - ANTHROPIC_VERTEX_PROJECT_ID: 'GCP Project ID', + CLAUDE_CODE_USE_OPENAI: 'Use OpenAI (flag)', CLAUDE_CODE_USE_GEMINI: 'Use Gemini (flag)', + CLAUDE_CODE_USE_BEDROCK: 'Use Bedrock (flag)', CLAUDE_CODE_USE_VERTEX: 'Use Vertex (flag)', + OPENAI_BASE_URL: 'Base URL', OPENAI_API_KEY: 'API Key', OPENAI_MODEL: 'Model', + GEMINI_API_KEY: 'API Key', GEMINI_MODEL: 'Model', AWS_REGION: 'AWS Region', + AWS_BEARER_TOKEN_BEDROCK: 'Bearer Token', ANTHROPIC_VERTEX_PROJECT_ID: 'GCP Project ID', CLOUD_ML_REGION: 'Region', } -// Provider accent colors const PROVIDER_COLORS: Record = { - anthropic: '#D4A574', - openrouter: '#6366F1', - openai: '#10A37F', - gemini: '#4285F4', - codex_auth: '#10A37F', - bedrock: '#FF9900', - vertex: '#4285F4', + anthropic: '#D4A574', openrouter: '#6366F1', openai: '#10A37F', + gemini: '#4285F4', codex_auth: '#10A37F', bedrock: '#FF9900', vertex: '#4285F4', } -function getColor(id: string) { - return PROVIDER_COLORS[id] || '#8b949e' -} +function isFlag(key: string) { return key.startsWith('CLAUDE_CODE_USE_') } +function isSecret(key: string) { return key.includes('KEY') || key.includes('SECRET') || key.includes('TOKEN') } -function isFlag(key: string) { - return key.startsWith('CLAUDE_CODE_USE_') -} - -function isSecret(key: string) { - return key.includes('KEY') || key.includes('SECRET') || key.includes('TOKEN') +/* ── Toggle switch ── */ +function Toggle({ on, onChange, disabled }: { on: boolean; onChange: (v: boolean) => void; disabled?: boolean }) { + return ( + + ) } export default function Providers() { const [providers, setProviders] = useState([]) + const [activeProvider, setActiveProvider] = useState('anthropic') const [loading, setLoading] = useState(true) const [configOpen, setConfigOpen] = useState(null) const [editVars, setEditVars] = useState({}) @@ -91,38 +65,49 @@ export default function Providers() { const [testResults, setTestResults] = useState>({}) const [claudeInstalled, setClaudeInstalled] = useState(false) const [openclaudeInstalled, setOpenclaudeInstalled] = useState(false) + const [codexAuth, setCodexAuth] = useState<{ authenticated: boolean; method?: string } | null>(null) + const [authModal, setAuthModal] = useState(false) + const [authMode, setAuthMode] = useState<'browser' | 'device'>('browser') + const [authUrl, setAuthUrl] = useState('') + const [callbackUrl, setCallbackUrl] = useState('') + const [authLoading, setAuthLoading] = useState(false) + const [authMessage, setAuthMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null) + const [deviceCode, setDeviceCode] = useState<{ user_code: string; verification_url: string; interval: number; expires_in: number } | null>(null) + const [devicePolling, setDevicePolling] = useState(false) + const [toggling, setToggling] = useState(null) const load = () => { setLoading(true) - api - .get('/providers') - .then((data: ProvidersResponse) => { - setProviders(data.providers || []) - setClaudeInstalled(data.claude_installed) - setOpenclaudeInstalled(data.openclaude_installed) - }) - .catch(() => setProviders([])) - .finally(() => setLoading(false)) + api.get('/providers').then((data: ProvidersResponse) => { + setProviders(data.providers || []) + setActiveProvider(data.active_provider || 'none') + setClaudeInstalled(data.claude_installed) + setOpenclaudeInstalled(data.openclaude_installed) + }).catch(() => setProviders([])).finally(() => setLoading(false)) } - useEffect(() => { load() }, []) + useEffect(() => { load(); loadCodexAuth() }, []) + useEffect(() => { + if (!devicePolling) return + const interval = (deviceCode?.interval || 5) * 1000 + const timer = setInterval(pollDeviceAuth, interval) + return () => clearInterval(timer) + }, [devicePolling]) - const handleActivate = async (id: string) => { + const handleToggle = async (id: string, turnOn: boolean) => { + setToggling(id) try { - await api.post('/providers/active', { provider_id: id }) + await api.post('/providers/active', { provider_id: turnOn ? id : 'none' }) load() - } catch (e) { - console.error(e) - } + } catch (e) { console.error(e) } + finally { setToggling(null) } } const openConfig = (prov: Provider) => { - // Load real env var values (API will return masked values for secrets) setConfigOpen(prov.id) - // Initialize with current values, replacing masked with empty for editing const vars: ProviderEnvVars = {} for (const [k, v] of Object.entries(prov.env_vars)) { - if (isFlag(k)) continue // Skip flags — they're automatic + if (isFlag(k)) continue vars[k] = v.includes('****') ? '' : v } setEditVars(vars) @@ -132,235 +117,170 @@ export default function Providers() { if (!configOpen) return setSaving(true) try { - // Find the provider to get defaults const prov = providers.find(p => p.id === configOpen) const finalVars = { ...editVars } - - // Auto-fill defaults if empty - if (prov?.default_base_url && !finalVars.OPENAI_BASE_URL) { - finalVars.OPENAI_BASE_URL = prov.default_base_url - } + if (prov?.default_base_url && !finalVars.OPENAI_BASE_URL) finalVars.OPENAI_BASE_URL = prov.default_base_url if (prov?.default_model) { - const modelKey = Object.keys(finalVars).find(k => k.includes('MODEL')) - if (modelKey && !finalVars[modelKey]) { - finalVars[modelKey] = prov.default_model - } + const mk = Object.keys(finalVars).find(k => k.includes('MODEL')) + if (mk && !finalVars[mk]) finalVars[mk] = prov.default_model } - - // Save env vars await api.post(`/providers/${configOpen}/config`, { env_vars: finalVars }) - // Activate as the current provider await api.post('/providers/active', { provider_id: configOpen }) setConfigOpen(null) load() - } catch (e) { - console.error(e) - } finally { - setSaving(false) - } + } catch (e) { console.error(e) } finally { setSaving(false) } } const handleTest = async (id: string) => { setTesting(id) try { const result = await api.post(`/providers/${id}/test`) as any - setTestResults(prev => ({ - ...prev, - [id]: { - success: result.success, - message: result.success - ? `${result.cli} ${result.version}` - : result.error || 'Test failed', - }, - })) - } catch (e) { - setTestResults(prev => ({ ...prev, [id]: { success: false, message: 'Request failed' } })) - } finally { - setTesting(null) - } + setTestResults(prev => ({ ...prev, [id]: { success: result.success, message: result.success ? `${result.cli} ${result.version}` : result.error || 'Test failed' } })) + } catch { setTestResults(prev => ({ ...prev, [id]: { success: false, message: 'Request failed' } })) } + finally { setTesting(null) } } - const activeCount = providers.filter(p => p.is_active).length + const loadCodexAuth = () => { api.get('/providers/openai/status').then((d: any) => setCodexAuth(d)).catch(() => setCodexAuth(null)) } + const startBrowserAuth = async () => { setAuthLoading(true); setAuthMessage(null); try { const d = await api.post('/providers/openai/auth-start') as any; setAuthUrl(d.authorize_url) } catch { setAuthMessage({ type: 'error', text: 'Failed to start auth' }) } finally { setAuthLoading(false) } } + const completeBrowserAuth = async () => { if (!callbackUrl.includes('code=')) { setAuthMessage({ type: 'error', text: 'Invalid URL - must contain ?code=' }); return }; setAuthLoading(true); try { const r = await api.post('/providers/openai/auth-complete', { callback_url: callbackUrl }) as any; if (r.status === 'ok') { setAuthMessage({ type: 'success', text: r.message || 'Authenticated' }); setAuthModal(false); loadCodexAuth(); load() } else { setAuthMessage({ type: 'error', text: r.error || 'Auth failed' }) } } catch { setAuthMessage({ type: 'error', text: 'Auth error' }) } finally { setAuthLoading(false) } } + const startDeviceAuth = async () => { setAuthLoading(true); setAuthMessage(null); try { const d = await api.post('/providers/openai/device-start') as any; if (d.error) setAuthMessage({ type: 'error', text: d.error }); else { setDeviceCode(d); setDevicePolling(true) } } catch { setAuthMessage({ type: 'error', text: 'Device auth not available' }) } finally { setAuthLoading(false) } } + const pollDeviceAuth = async () => { try { const r = await api.post('/providers/openai/device-poll') as any; if (r.status === 'authorized') { setDevicePolling(false); setDeviceCode(null); setAuthModal(false); loadCodexAuth(); load() } } catch {} } + const handleOpenAILogout = async () => { try { await api.post('/providers/openai/logout'); setCodexAuth({ authenticated: false }); load() } catch {} } + const configuredCount = providers.filter(p => p.has_config && p.installed).length + const hasActive = activeProvider !== 'none' && providers.some(p => p.id === activeProvider) + + const inp = "w-full px-4 py-3 rounded-lg bg-[#0f1520] border border-[#1e2a3a] text-[#e2e8f0] placeholder-[#3d4f65] text-sm transition-colors duration-200 focus:outline-none focus:border-[#00FFA7]/60 focus:ring-1 focus:ring-[#00FFA7]/20 font-mono" + const lbl = "block text-[11px] font-semibold text-[#5a6b7f] mb-1.5 tracking-[0.08em] uppercase" return ( -
+
{/* Header */} -
-

Providers

-

- Configure which AI provider powers EvoNexus — Anthropic (native), OpenRouter, OpenAI, Gemini, and more -

+
+

Providers

+

Configure and activate AI providers for your workspace

- {/* Install status banner */} + {/* Status bar */} {!loading && ( -
- - {claudeInstalled ? : } - claude {claudeInstalled ? 'installed' : 'not found'} - - - {openclaudeInstalled ? : } - openclaude {openclaudeInstalled ? 'installed' : ( - - — npm install -g @gitlawb/openclaude - - )} - +
+
+ + + claude {claudeInstalled ? '' : '(missing)'} + + + + openclaude {openclaudeInstalled ? '' : '(missing)'} + +
+
+ {providers.length} available + {configuredCount} configured + + {hasActive ? '1 active' : 'none active'} + +
)} - {/* Stats */} -
- {loading ? ( - <> -
-
-
- - ) : ( - <> -
-

{providers.length}

-

Available

-
-
-

{configuredCount}

-

Configured

-
-
-

{activeCount}

-

Active

-
- - )} -
- - {/* Provider Cards */} + {/* Provider list */} {loading ? ( -
- {[...Array(4)].map((_, i) => ( -
- ))} +
+ {[...Array(3)].map((_, i) =>
)}
) : ( -
+
{providers.map((prov) => { - const color = getColor(prov.id) + const color = PROVIDER_COLORS[prov.id] || '#5a6b7f' const isInstalled = prov.cli_command === 'claude' ? claudeInstalled : openclaudeInstalled + const isActive = prov.is_active && activeProvider === prov.id return ( -
- {/* Active indicator */} - {prov.is_active && ( -
- )} +
+ {/* Color dot */} +
- {/* Header */} -
-
+ {/* Info */} +
-
-

{prov.name}

+

{prov.name}

+ + {prov.cli_command} + + {!isInstalled && ( + + not installed + + )}
-

{prov.description}

+

{prov.description}

- {prov.is_active && ( - - Active - - )} -
- {/* CLI badge */} -
- - {prov.cli_command} - - - {isInstalled ? 'installed' : 'not found'} - - {prov.has_config && Object.keys(prov.env_vars).length > 0 && ( - - configured + {/* OpenAI auth badge */} + {prov.id === 'openai' && codexAuth?.authenticated && ( + + OAuth )} -
- {/* Logout warning */} - {prov.requires_logout && prov.is_active && ( -

- Run /logout in Claude Code if you were previously logged into Anthropic -

- )} + {/* Actions */} +
+ {/* OpenAI login */} + {prov.id === 'openai' && isInstalled && !codexAuth?.authenticated && ( + + )} + {prov.id === 'openai' && codexAuth?.authenticated && ( + + )} - {/* Actions */} -
- + {/* Configure */} + - {!prov.is_active && isInstalled && ( - - )} - + {/* Toggle switch */} + handleToggle(prov.id, on)} + /> +
- {/* Test result inline */} - {testResults[prov.id] && configOpen !== prov.id && testing !== prov.id && ( -
{testResults[prov.id].message}
)} + + {/* Logout warning */} + {prov.requires_logout && isActive && ( +
+ Run /logout in Claude Code if you were previously logged into Anthropic +
+ )}
) })} @@ -374,121 +294,70 @@ export default function Providers() { const editableVars = Object.entries(prov.env_vars).filter(([k]) => !isFlag(k)) return ( -
-
- {/* Modal header */} -
-
-
-

- Configure {prov.name} -

+
+
+
+
+
+

{prov.name}

-
- {/* Modal body */}
{editableVars.length === 0 ? ( -

- No configuration needed — uses native Claude Code authentication. -

+

No configuration needed. Uses native Claude Code authentication.

) : ( editableVars.map(([key]) => (
-
)) )} - {/* Defaults hint */} {prov.default_model && ( -

- Default model: {prov.default_model} - {prov.default_base_url && ( - <> | Base URL: {prov.default_base_url} - )} +

+ Default model: {prov.default_model} + {prov.default_base_url && <> | URL: {prov.default_base_url}}

)} - {/* Setup hint */} - {prov.setup_hint && ( -
-

{prov.setup_hint}

-
- )} - - {/* Logout warning */} {prov.requires_logout && ( -
-

- After activating, run /logout inside Claude Code - if you were previously logged into Anthropic. -

+
+

After activating, run /logout in Claude Code if previously logged into Anthropic.

)} - {/* Test result */} {testResults[prov.id] && ( -
+
{testResults[prov.id].success ? : } {testResults[prov.id].message}
)}
- {/* Modal footer */} -
-
- -
@@ -496,6 +365,90 @@ export default function Providers() {
) })()} + + {/* OpenAI Auth Modal */} + {authModal && ( +
+
+
+

Connect to OpenAI

+ +
+ +
+ + +
+ +
+ {authMode === 'browser' ? ( + authUrl ? ( + <> +
+

1. Open this link to login:

+ + Open OpenAI Login + +
+
+

2. Authorize access, then copy the URL from the error page:

+ setCallbackUrl(e.target.value)} + placeholder="http://localhost:1455/auth/callback?code=..." + className={inp} autoComplete="off" /> +
+ + ) : ( +
+ Generating auth link... +
+ ) + ) : ( + deviceCode ? ( +
+

1. Open: {deviceCode.verification_url}

+

2. Enter code:

+
+ {deviceCode.user_code} +
+ {devicePolling &&

Waiting for authorization...

} +
+ ) : authLoading ? ( +
Starting device auth...
+ ) : null + )} + + {authMessage && ((authMode === 'browser') || (authMode === 'device' && authMessage.type === 'error')) && ( +
+ {authMessage.text} + {authMessage.type === 'error' && authMode === 'device' &&

Your organization may not allow Device Auth. Use Browser OAuth instead.

} +
+ )} +
+ +
+ + {authMode === 'browser' && authUrl && ( + + )} +
+
+
+ )}
) } diff --git a/dashboard/frontend/src/pages/Setup.tsx b/dashboard/frontend/src/pages/Setup.tsx index 3dbcd93b..51e627d1 100644 --- a/dashboard/frontend/src/pages/Setup.tsx +++ b/dashboard/frontend/src/pages/Setup.tsx @@ -1,18 +1,94 @@ -import { useState, useEffect, type FormEvent } from 'react' +import { useState, useEffect, useRef, useCallback, type FormEvent } from 'react' import { useAuth } from '../context/AuthContext' import { api } from '../lib/api' +/* ── Animated mesh background ── */ +function NetworkCanvas() { + const ref = useRef(null) + + useEffect(() => { + const canvas = ref.current + if (!canvas) return + const ctx = canvas.getContext('2d') + if (!ctx) return + + let animId: number + let particles: { x: number; y: number; vx: number; vy: number }[] = [] + + const resize = () => { + canvas.width = window.innerWidth + canvas.height = window.innerHeight + } + + const init = () => { + resize() + const count = Math.floor((canvas.width * canvas.height) / 18000) + particles = Array.from({ length: Math.min(count, 80) }, () => ({ + x: Math.random() * canvas.width, + y: Math.random() * canvas.height, + vx: (Math.random() - 0.5) * 0.3, + vy: (Math.random() - 0.5) * 0.3, + })) + } + + const draw = () => { + ctx.clearRect(0, 0, canvas.width, canvas.height) + const maxDist = 150 + + for (let i = 0; i < particles.length; i++) { + const p = particles[i] + p.x += p.vx + p.y += p.vy + if (p.x < 0 || p.x > canvas.width) p.vx *= -1 + if (p.y < 0 || p.y > canvas.height) p.vy *= -1 + + // Node + ctx.beginPath() + ctx.arc(p.x, p.y, 1.5, 0, Math.PI * 2) + ctx.fillStyle = 'rgba(0, 255, 167, 0.25)' + ctx.fill() + + // Edges + for (let j = i + 1; j < particles.length; j++) { + const q = particles[j] + const dx = p.x - q.x + const dy = p.y - q.y + const dist = Math.sqrt(dx * dx + dy * dy) + if (dist < maxDist) { + ctx.beginPath() + ctx.moveTo(p.x, p.y) + ctx.lineTo(q.x, q.y) + ctx.strokeStyle = `rgba(0, 255, 167, ${0.06 * (1 - dist / maxDist)})` + ctx.lineWidth = 0.5 + ctx.stroke() + } + } + } + animId = requestAnimationFrame(draw) + } + + init() + draw() + window.addEventListener('resize', init) + return () => { + cancelAnimationFrame(animId) + window.removeEventListener('resize', init) + } + }, []) + + return +} + +/* ── Main ── */ export default function Setup() { const { refreshUser } = useAuth() const [hasConfig, setHasConfig] = useState(null) - // Step 1: Workspace (only if no config exists) const [ownerName, setOwnerName] = useState('') const [companyName, setCompanyName] = useState('') const [timezone, setTimezone] = useState('America/Sao_Paulo') const [language, setLanguage] = useState('en') - // Step 2: Admin account const [username, setUsername] = useState('') const [email, setEmail] = useState('') const [displayName, setDisplayName] = useState('') @@ -21,30 +97,27 @@ export default function Setup() { const [error, setError] = useState('') const [submitting, setSubmitting] = useState(false) + const [currentStep, setCurrentStep] = useState(1) - // Check if workspace.yaml already exists (CLI setup done) useEffect(() => { api.get('/config/workspace-status').then((data: { configured: boolean }) => { setHasConfig(data.configured) }).catch(() => setHasConfig(false)) }, []) - const [currentStep, setCurrentStep] = useState(1) - - // If config exists, skip to step 2 useEffect(() => { if (hasConfig === true) setCurrentStep(2) }, [hasConfig]) - const handleStep1 = (e: FormEvent) => { + const handleStep1 = useCallback((e: FormEvent) => { e.preventDefault() - if (!ownerName.trim()) { setError('Your name is required'); return } + if (!ownerName.trim()) { setError('Name is required'); return } setError('') setDisplayName(ownerName) setCurrentStep(2) - } + }, [ownerName]) - const handleStep2 = async (e: FormEvent) => { + const handleStep2 = useCallback(async (e: FormEvent) => { e.preventDefault() setError('') if (!username.trim()) { setError('Username is required'); return } @@ -53,34 +126,15 @@ export default function Setup() { setSubmitting(true) try { - // Collect geo (silent, best-effort) let geo = {} try { - const geoResp = await fetch('https://ipapi.co/json/', { signal: AbortSignal.timeout(5000) }) - if (geoResp.ok) { - const geoData = await geoResp.json() - geo = { - country: geoData.country_name, - country_code: geoData.country_code, - city: geoData.city, - region: geoData.region, - lat: geoData.latitude, - lng: geoData.longitude, - timezone: geoData.timezone, - } - } - } catch { /* geo is optional */ } + const r = await fetch('https://ipapi.co/json/', { signal: AbortSignal.timeout(5000) }) + if (r.ok) { const d = await r.json(); geo = { country: d.country_name, country_code: d.country_code, city: d.city, region: d.region, lat: d.latitude, lng: d.longitude, timezone: d.timezone } } + } catch { /* optional */ } await api.post('/auth/setup', { - // Only send workspace config if not already configured via CLI - workspace: hasConfig ? undefined : { - owner_name: ownerName.trim(), - company_name: companyName.trim(), - timezone, - language, - agents: [], - integrations: [], - geo, + workspace: (hasConfig && currentStep === 2 && !ownerName.trim()) ? undefined : { + owner_name: ownerName.trim(), company_name: companyName.trim(), timezone, language, agents: [], integrations: [], geo, }, username: username.trim(), email: email.trim() || undefined, @@ -88,136 +142,196 @@ export default function Setup() { password, }) await refreshUser() + window.location.href = '/providers' } catch (ex: unknown) { setError(ex instanceof Error ? ex.message : 'Setup failed') } finally { setSubmitting(false) } - } - - const inputClass = "w-full px-4 py-2.5 rounded-lg bg-[#0C111D] border border-[#344054] text-white placeholder-[#667085] focus:outline-none focus:border-[#00FFA7] focus:ring-1 focus:ring-[#00FFA7] transition-colors text-sm" + }, [hasConfig, currentStep, ownerName, companyName, timezone, language, username, email, displayName, password, confirmPassword, refreshUser]) if (hasConfig === null) return ( -
-
Loading...
+
+
) + const inp = "w-full px-4 py-3 rounded-lg bg-[#0f1520] border border-[#1e2a3a] text-[#e2e8f0] placeholder-[#3d4f65] text-sm transition-colors duration-200 focus:outline-none focus:border-[#00FFA7]/60 focus:ring-1 focus:ring-[#00FFA7]/20" + const lbl = "block text-[11px] font-semibold text-[#5a6b7f] mb-1.5 tracking-[0.08em] uppercase" + return ( -
-
-
- {/* Logo */} -
-

- Evo - Nexus -

-

- {currentStep === 1 ? 'Configure your workspace' : 'Create your admin account'} -

+
+ + +
+ {/* ── Card ── */} +
+ + {/* Header */} +
+
+ {/* Logo mark */} +
+ + + + + +
+
+

+ EvoNexus +

+

AI Workspace Platform

+
+
+ + {/* Step nav */} {!hasConfig && ( -
-
= 1 ? 'bg-[#00FFA7]' : 'bg-[#344054]'}`} /> -
= 2 ? 'bg-[#00FFA7]' : 'bg-[#344054]'}`} /> +
+ +
)}
- {error && ( -
- {error} -
- )} - - {/* Step 1: Workspace config (only if CLI setup was not done) */} - {currentStep === 1 && !hasConfig && ( -
-
- - setOwnerName(e.target.value)} - className={inputClass} placeholder="John Doe" autoFocus /> -
-
- - setCompanyName(e.target.value)} - className={inputClass} placeholder="Acme Inc" /> + {/* Form */} +
+ {error && ( +
+ {error}
-
+ )} + + {/* Step 1 */} + {currentStep === 1 && !hasConfig && ( +
- - setTimezone(e.target.value)} - className={inputClass} placeholder="America/Sao_Paulo" /> + + setOwnerName(e.target.value)} + className={inp} placeholder="Full name" autoFocus autoComplete="name" />
- - + + setCompanyName(e.target.value)} + className={inp} placeholder="Organization name" autoComplete="organization" /> +
+
+
+ + setTimezone(e.target.value)} + className={inp} /> +
+
+ + +
-
- - - )} + + + )} - {/* Step 2: Admin Account */} - {currentStep === 2 && ( -
-
- - setUsername(e.target.value)} - className={inputClass} placeholder="admin" autoFocus /> -
-
- - setEmail(e.target.value)} - className={inputClass} placeholder="admin@example.com" /> -
-
- - setDisplayName(e.target.value)} - className={inputClass} placeholder={ownerName || 'Admin'} /> -
-
- - setPassword(e.target.value)} - className={inputClass} placeholder="Min 6 characters" /> -
-
- - setConfirmPassword(e.target.value)} - className={inputClass} placeholder="Repeat password" /> -
+ {/* Step 2 */} + {currentStep === 2 && ( + + {hasConfig && ( +

Create your administrator account to get started.

+ )} +
+ + setUsername(e.target.value)} + className={inp} placeholder="admin" autoFocus autoComplete="username" /> +
+
+ + setEmail(e.target.value)} + className={inp} placeholder="you@company.com" autoComplete="email" /> +
+
+ + setDisplayName(e.target.value)} + className={inp} placeholder={ownerName || 'Your name'} autoComplete="name" /> +
+
+
+ + setPassword(e.target.value)} + className={inp} placeholder="Min 6 chars" autoComplete="new-password" /> +
+
+ + setConfirmPassword(e.target.value)} + className={inp} placeholder="Repeat" autoComplete="new-password" /> +
+
-
- {!hasConfig && ( - + )} + - )} - -
-
- )} +
+ + )} +
+ + {/* Footer stats */} +
+
+ 38 Agents + 137 Skills + Multi-AI +
+
+
- {/* Credits */} - +

) diff --git a/dashboard/terminal-server/src/claude-bridge.js b/dashboard/terminal-server/src/claude-bridge.js index 9c7ee760..e8335a93 100644 --- a/dashboard/terminal-server/src/claude-bridge.js +++ b/dashboard/terminal-server/src/claude-bridge.js @@ -29,8 +29,7 @@ class ClaudeBridge { const configPath = path.join(workspaceRoot, 'config', 'providers.json'); if (!fs.existsSync(configPath)) { console.log(`[provider] providers.json not found at ${configPath}, using defaults`); - return { cli_command: 'claude', env_vars: {} }; - } + return { cli_command: 'claude', env_vars: {}, active: 'anthropic' }; } const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); const active = config.active_provider || 'anthropic'; const provider = config.providers?.[active] || {}; @@ -47,14 +46,24 @@ class ClaudeBridge { ) ); + // If Codex OAuth auth.json exists, remove OPENAI_API_KEY to let + // OpenClaude use the OAuth token instead of a potentially stale key + if (active === 'openai' || active === 'codex_auth') { + const codexAuthPath = path.join(process.env.HOME || '/', '.codex', 'auth.json'); + if (fs.existsSync(codexAuthPath)) { + delete envVars['OPENAI_API_KEY']; + console.log('[provider] Codex auth.json found — using OAuth token, removing OPENAI_API_KEY'); + } + } + console.log(`[provider] Active provider: ${active} (cli: ${cliCommand})`); if (Object.keys(envVars).length > 0) { console.log(`[provider] Injecting env vars: ${Object.keys(envVars).join(', ')}`); } - return { cli_command: cliCommand, env_vars: envVars }; + return { cli_command: cliCommand, env_vars: envVars, active }; } catch (err) { console.warn(`[provider] Could not read providers.json: ${err.message}`); - return { cli_command: 'claude', env_vars: {} }; + return { cli_command: 'claude', env_vars: {}, active: 'anthropic' }; } } @@ -128,25 +137,93 @@ class ClaudeBridge { // Reload provider config fresh on every session start // so switching provider in the dashboard takes effect immediately const providerConfig = this._loadProviderConfig(); + + // Block session if no provider is active + if (!providerConfig.active || providerConfig.active === 'none') { + const msg = '\r\n\x1b[1;33mNo AI provider is active.\x1b[0m\r\nGo to \x1b[1;32mProviders\x1b[0m in the dashboard to configure and activate a provider.\r\n'; + if (onOutput) onOutput(msg); + if (onExit) onExit(1, null); + return; + } + const cliCommand = this.findClaudeCommand(providerConfig.cli_command); console.log(`Starting session ${sessionId} with ${providerConfig.cli_command}`); console.log(`Command: ${cliCommand}`); console.log(`Working directory: ${workingDir}`); + console.log(`Agent: ${agent || 'none'}`); console.log(`Terminal size: ${cols}x${rows}`); if (dangerouslySkipPermissions) { console.log(`⚠️ WARNING: Skipping permissions with --dangerously-skip-permissions flag`); } - const args = dangerouslySkipPermissions ? ['--dangerously-skip-permissions'] : []; + // Don't use --dangerously-skip-permissions when running as root — + // Claude/OpenClaude block this flag for root users. + // The trust prompt is auto-accepted via PTY detection below instead. + const isRoot = process.getuid && process.getuid() === 0; + const args = (dangerouslySkipPermissions && !isRoot) ? ['--dangerously-skip-permissions'] : []; if (agent) { args.push('--agent', agent); } + + // For non-Anthropic providers, use --system-prompt to force agent persona. + // --append-system-prompt is too weak — GPT models ignore appended instructions. + // --system-prompt REPLACES the default system prompt, ensuring the agent persona + // takes priority over CLAUDE.md and other context that mentions "Claude". + const active = providerConfig.active || 'anthropic'; + if (active !== 'anthropic' && agent) { + // Read the agent definition file to build a strong system prompt + const agentFile = path.join(workingDir, '.claude', 'agents', `${agent}.md`); + let agentPrompt = ''; + try { + const content = fs.readFileSync(agentFile, 'utf8'); + // Extract body (after YAML frontmatter ---) + const match = content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/); + agentPrompt = match ? match[1].trim() : content; + } catch { + agentPrompt = `You are the ${agent} agent.`; + } + + const enforcePrompt = agentPrompt + '\n\n' + + 'CRITICAL: You MUST fully embody this agent persona. ' + + 'You are NOT Claude, OpenClaude, or a generic assistant — you ARE ' + agent + '. ' + + 'When asked who you are, ALWAYS respond as ' + agent + '. ' + + 'Never break character. Follow ALL instructions above.'; + + args.push('--system-prompt', enforcePrompt); + } const providerEnv = providerConfig.env_vars || {}; + + // Build a CLEAN environment for the spawned CLI process. + // We DON'T spread process.env — it may contain stale/cached vars + // (OPENAI_API_KEY, etc.) that override Codex OAuth auth.json. + // Instead, whitelist only essential system vars + provider config. + const SYSTEM_VARS = [ + 'HOME', 'USER', 'SHELL', 'PATH', 'LANG', 'LC_ALL', 'LC_CTYPE', + 'LOGNAME', 'HOSTNAME', 'XDG_RUNTIME_DIR', 'XDG_DATA_HOME', + 'XDG_CONFIG_HOME', 'XDG_CACHE_HOME', 'TMPDIR', + 'SSH_AUTH_SOCK', 'SSH_AGENT_PID', + 'NVM_DIR', 'NVM_BIN', 'NVM_INC', + 'CODEX_HOME', 'CLAUDE_CONFIG_DIR', + ]; + const cleanEnv = {}; + for (const key of SYSTEM_VARS) { + if (process.env[key]) cleanEnv[key] = process.env[key]; + } + + // Ensure OPENAI_MODEL is set when using OpenAI provider. + // Without it, OpenClaude can't resolve which model to use and + // falls back to API key auth instead of Codex OAuth auth.json. + if ((active === 'openai' || active === 'codex_auth') && !providerEnv['OPENAI_MODEL']) { + providerEnv['OPENAI_MODEL'] = 'gpt-5.4'; + console.log('[provider] OPENAI_MODEL not set — defaulting to gpt-5.4'); + } + + console.log(`[spawn] Args: ${JSON.stringify(args)}`); const claudeProcess = spawn(cliCommand, args, { cwd: workingDir, env: { - ...process.env, + ...cleanEnv, ...providerEnv, TERM: 'xterm-256color', FORCE_COLOR: '1', diff --git a/dashboard/terminal-server/src/server.js b/dashboard/terminal-server/src/server.js index 5aa33bc6..b6520529 100644 --- a/dashboard/terminal-server/src/server.js +++ b/dashboard/terminal-server/src/server.js @@ -372,8 +372,15 @@ class TerminalServer { const sessionId = wsInfo.claudeSessionId; try { + // Ensure agent name from session is passed even if options don't include it + const agentForSession = (options && options.agent) || session.agentName || null; + if (this.dev) console.log(`Starting agent: ${agentForSession} for session ${sessionId}`); + + console.log(`[startClaude] Agent for session: ${agentForSession}, options.agent: ${options?.agent}`); await this.claudeBridge.startSession(sessionId, { + ...options, workingDir: session.workingDir, + agent: agentForSession, onOutput: (data) => { const currentSession = this.claudeSessions.get(sessionId); if (!currentSession) return; @@ -393,7 +400,6 @@ class TerminalServer { if (currentSession) currentSession.active = false; this.broadcastToSession(sessionId, { type: 'error', message: error.message }); }, - ...options, }); session.active = true; diff --git a/docs/dashboard/env-editor.md b/docs/dashboard/env-editor.md index 7aafc40c..602b6762 100644 --- a/docs/dashboard/env-editor.md +++ b/docs/dashboard/env-editor.md @@ -2,7 +2,7 @@ The dashboard includes a built-in editor for the `.env` file, accessible from **Config** in the sidebar. This lets you manage API keys and integration settings without touching the terminal. -![Config](../imgs/doc-config.png) +![Config](../imgs/doc-config.webp) Requires the `config:manage` permission (admin role by default). diff --git a/docs/dashboard/knowledge-base.md b/docs/dashboard/knowledge-base.md index 80421fad..c559f97c 100644 --- a/docs/dashboard/knowledge-base.md +++ b/docs/dashboard/knowledge-base.md @@ -6,7 +6,7 @@ The Knowledge Base page provides semantic search over your code, documentation, Everything runs locally — no external APIs, no data leaves your machine. -![Knowledge Base](../imgs/doc-knowledge.png) +![Knowledge Base](../imgs/doc-knowledge.webp) ## Enabling MemPalace diff --git a/docs/dashboard/overview.md b/docs/dashboard/overview.md index e2dd01f4..f2d0c3f2 100644 --- a/docs/dashboard/overview.md +++ b/docs/dashboard/overview.md @@ -42,71 +42,71 @@ After setup completes, you are logged in as admin and can access all pages. Unified metrics dashboard. Shows aggregated data from all agents -- financial snapshot, community health, project status, social reach. This is the landing page after login. -![Overview](../imgs/doc-overview.png) +![Overview](../imgs/doc-overview.webp) ### Systems Register external apps and services your team uses. Each system has a name, URL, type (Docker container, external URL, or iframe), and icon. Useful for quick-access links to tools like Grafana, Portainer, or internal apps. -![Systems list](../imgs/doc-systems-list.png) +![Systems list](../imgs/doc-systems-list.webp) -![Systems app](../imgs/doc-systems-app.png) +![Systems app](../imgs/doc-systems-app.webp) ### Reports Browse HTML reports generated by automated routines. Reports are stored in `workspace/` subfolders and displayed with date, agent, and type. Click any report to view the full HTML in a new tab. -![Reports list](../imgs/doc-reports-list.png) +![Reports list](../imgs/doc-reports-list.webp) -![Reports open](../imgs/doc-reports-open.png) +![Reports open](../imgs/doc-reports-open.webp) ### Agents View the 16 agent definitions. Each card shows the agent name, slash command, domain, and full system prompt (loaded from `.claude/agents/`). Core agents have dedicated icons and colors; custom agents show a gray badge. -![Agents](../imgs/doc-agents.png) +![Agents](../imgs/doc-agents.webp) ### Routines Metrics for each automated routine: total runs, success rate, average duration, token usage, and cost. Includes a "Run Now" button to trigger any routine manually. -![Routines](../imgs/doc-routines.png) +![Routines](../imgs/doc-routines.webp) ### Tasks Create and manage one-off scheduled actions. Schedule a skill, prompt, or script to run at a specific date/time. Filter by status (pending, running, completed, failed), create new tasks, run immediately, or view results. See [Scheduled Tasks](../routines/scheduled-tasks.md) for details. -![Tasks](../imgs/doc-tasks.png) +![Tasks](../imgs/doc-tasks.webp) ### Triggers Reactive event triggers -- webhook and event-based. Create triggers that execute skills or routines in response to external events (GitHub push, Stripe payment, Linear updates). Filter by type (webhooks, events), status (enabled, disabled), and manage trigger configurations. -![Triggers](../imgs/doc-triggers.png) +![Triggers](../imgs/doc-triggers.webp) ### Skills Browse all installed skills grouped by prefix (`social-`, `fin-`, `int-`, `prod-`, etc). Click a skill to see its full description, trigger conditions, and source file. -![Skills](../imgs/doc-skills.png) +![Skills](../imgs/doc-skills.webp) ### Templates Preview the HTML report templates from `.claude/templates/html/`. See how each template renders before routines use them. -![Templates](../imgs/doc-templates.png) +![Templates](../imgs/doc-templates.webp) ### Services Start and stop background services (scheduler, Telegram bot) directly from the UI. Shows live log output via WebSocket streaming. Status indicators show whether each service is running. -![Services](../imgs/doc-services.png) +![Services](../imgs/doc-services.webp) ### Workspace Browse workspace reports and output files organized by domain (community, courses, daily-logs, finance, meetings, personal, projects, social, strategy). Navigate folders, filter files, and open reports directly from the dashboard. -![Workspace](../imgs/doc-workspace.png) +![Workspace](../imgs/doc-workspace.webp) ### Memory @@ -115,19 +115,19 @@ Browse the two-tier memory system: - **memory/** -- detailed files (people, projects, glossary, trends) - **Agent memory** -- per-agent context in `.claude/agent-memory/` -![Memory](../imgs/doc-memory.png) +![Memory](../imgs/doc-memory.webp) ### Knowledge Base Optional semantic search powered by [MemPalace](https://github.com/milla-jovovich/mempalace). Enable it with one click, add directories (code, docs, notes) as sources, index them, and search by meaning -- not just keywords. Everything runs locally using ChromaDB vectors. See [knowledge-base.md](knowledge-base.md) for details. -![Knowledge Base](../imgs/doc-knowledge.png) +![Knowledge Base](../imgs/doc-knowledge.webp) ### Integrations Status board for all 18 integrations. Shows which are connected (green), which need configuration (yellow), and which are disabled. Social media accounts (YouTube, Instagram, LinkedIn) can be connected via OAuth directly from this page. -![Integrations](../imgs/doc-integrations.png) +![Integrations](../imgs/doc-integrations.webp) ### Providers @@ -137,31 +137,31 @@ Pick and configure which LLM backend powers EvoNexus — Anthropic (default), or Embedded Claude Code terminal powered by xterm.js + WebSocket. Run Claude Code commands, invoke agents with slash commands, and see output in real time -- all from the browser. -![Chat](../imgs/doc-chat.png) +![Chat](../imgs/doc-chat.webp) ### Users User management page (admin only). Create, edit, deactivate users. Assign roles. See last login timestamps. -![Users](../imgs/doc-users.png) +![Users](../imgs/doc-users.webp) ### Roles Define custom roles with a granular permission matrix. Each role maps resources (chat, services, reports, etc.) to actions (view, execute, manage). Built-in roles (admin, operator, viewer) cannot be deleted but can be cloned. -![Roles](../imgs/doc-roles.png) +![Roles](../imgs/doc-roles.webp) ### Costs Token usage and cost tracking per routine. Displays charts showing cost trends over time, token consumption breakdown (input vs output), and per-routine cost comparison. Useful for monitoring Claude API spend across automated workflows. -![Costs](../imgs/doc-costs.png) +![Costs](../imgs/doc-costs.webp) ### Files Browse workspace files directly from the dashboard. Navigate the folder structure, preview file contents, and understand how the workspace is organized without needing terminal access. -![Files](../imgs/doc-files.png) +![Files](../imgs/doc-files.webp) ### Scheduler @@ -171,7 +171,7 @@ Manage background services and scheduled routines. Shows all registered routines Full audit trail of all actions: logins, config changes, routine executions, user management. Filterable by user, action, resource, and date range. -![Audit Log](../imgs/doc-audit-log.png) +![Audit Log](../imgs/doc-audit-log.webp) ### Backups @@ -179,7 +179,7 @@ Export and restore workspace data (all gitignored user files). Create local back ### Config -![Config](../imgs/doc-config.png) +![Config](../imgs/doc-config.webp) View and edit workspace configuration: - **CLAUDE.md** -- rendered markdown viewer diff --git a/docs/dashboard/users-and-roles.md b/docs/dashboard/users-and-roles.md index e073fc44..0d1a19b3 100644 --- a/docs/dashboard/users-and-roles.md +++ b/docs/dashboard/users-and-roles.md @@ -51,7 +51,7 @@ Built-in roles cannot be deleted, but you can create custom roles with any permi The new user can immediately log in at the dashboard URL. -![User Management](../imgs/doc-users.png) +![User Management](../imgs/doc-users.webp) ### First User (Setup Wizard) @@ -67,7 +67,7 @@ The very first user is created during the setup wizard when the dashboard starts 4. Use the permission matrix to toggle actions per resource 5. Click **Save** -![Roles permission matrix](../imgs/doc-roles.png) +![Roles permission matrix](../imgs/doc-roles.webp) ### Permission Matrix diff --git a/docs/getting-started.md b/docs/getting-started.md index 03c3a533..c4765417 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -20,7 +20,7 @@ This downloads and runs the interactive setup wizard automatically. ### Alternative: Manual Clone ```bash -git clone https://github.com/EvolutionAPI/evo-nexus.git +git clone --depth 1 https://github.com/EvolutionAPI/evo-nexus.git cd evo-nexus # Interactive setup wizard @@ -78,7 +78,7 @@ make dashboard-app Open http://localhost:8080 — the first run shows a setup wizard where you create your admin account and configure the workspace. -![Dashboard](imgs/doc-overview.png) +![Dashboard](imgs/doc-overview.webp) ### 5. Start Automated Routines diff --git a/docs/imgs/doc-agents.png b/docs/imgs/doc-agents.png deleted file mode 100644 index 0cf3cf0f..00000000 Binary files a/docs/imgs/doc-agents.png and /dev/null differ diff --git a/docs/imgs/doc-agents.webp b/docs/imgs/doc-agents.webp new file mode 100644 index 00000000..358631df Binary files /dev/null and b/docs/imgs/doc-agents.webp differ diff --git a/docs/imgs/doc-audit-log.png b/docs/imgs/doc-audit-log.png deleted file mode 100644 index 83b5d7dd..00000000 Binary files a/docs/imgs/doc-audit-log.png and /dev/null differ diff --git a/docs/imgs/doc-audit-log.webp b/docs/imgs/doc-audit-log.webp new file mode 100644 index 00000000..41a992aa Binary files /dev/null and b/docs/imgs/doc-audit-log.webp differ diff --git a/docs/imgs/doc-chat.png b/docs/imgs/doc-chat.png deleted file mode 100644 index 7e87ecd8..00000000 Binary files a/docs/imgs/doc-chat.png and /dev/null differ diff --git a/docs/imgs/doc-chat.webp b/docs/imgs/doc-chat.webp new file mode 100644 index 00000000..51fe0708 Binary files /dev/null and b/docs/imgs/doc-chat.webp differ diff --git a/docs/imgs/doc-config.png b/docs/imgs/doc-config.png deleted file mode 100644 index a839417d..00000000 Binary files a/docs/imgs/doc-config.png and /dev/null differ diff --git a/docs/imgs/doc-config.webp b/docs/imgs/doc-config.webp new file mode 100644 index 00000000..cd31b525 Binary files /dev/null and b/docs/imgs/doc-config.webp differ diff --git a/docs/imgs/doc-costs.png b/docs/imgs/doc-costs.png deleted file mode 100644 index ff579993..00000000 Binary files a/docs/imgs/doc-costs.png and /dev/null differ diff --git a/docs/imgs/doc-costs.webp b/docs/imgs/doc-costs.webp new file mode 100644 index 00000000..2044fea7 Binary files /dev/null and b/docs/imgs/doc-costs.webp differ diff --git a/docs/imgs/doc-files.png b/docs/imgs/doc-files.png deleted file mode 100644 index aaf6be8f..00000000 Binary files a/docs/imgs/doc-files.png and /dev/null differ diff --git a/docs/imgs/doc-files.webp b/docs/imgs/doc-files.webp new file mode 100644 index 00000000..388f9f62 Binary files /dev/null and b/docs/imgs/doc-files.webp differ diff --git a/docs/imgs/doc-integrations.png b/docs/imgs/doc-integrations.png deleted file mode 100644 index 52d7faca..00000000 Binary files a/docs/imgs/doc-integrations.png and /dev/null differ diff --git a/docs/imgs/doc-integrations.webp b/docs/imgs/doc-integrations.webp new file mode 100644 index 00000000..6d57a2db Binary files /dev/null and b/docs/imgs/doc-integrations.webp differ diff --git a/docs/imgs/doc-knowledge.png b/docs/imgs/doc-knowledge.png deleted file mode 100644 index f343ca02..00000000 Binary files a/docs/imgs/doc-knowledge.png and /dev/null differ diff --git a/docs/imgs/doc-knowledge.webp b/docs/imgs/doc-knowledge.webp new file mode 100644 index 00000000..dd7ce8d5 Binary files /dev/null and b/docs/imgs/doc-knowledge.webp differ diff --git a/docs/imgs/doc-memory.png b/docs/imgs/doc-memory.png deleted file mode 100644 index 7231bc1b..00000000 Binary files a/docs/imgs/doc-memory.png and /dev/null differ diff --git a/docs/imgs/doc-memory.webp b/docs/imgs/doc-memory.webp new file mode 100644 index 00000000..cb85da6a Binary files /dev/null and b/docs/imgs/doc-memory.webp differ diff --git a/docs/imgs/doc-overview.png b/docs/imgs/doc-overview.png deleted file mode 100644 index 0fd5214f..00000000 Binary files a/docs/imgs/doc-overview.png and /dev/null differ diff --git a/docs/imgs/doc-overview.webp b/docs/imgs/doc-overview.webp new file mode 100644 index 00000000..cd797f2c Binary files /dev/null and b/docs/imgs/doc-overview.webp differ diff --git a/docs/imgs/doc-reports-list.png b/docs/imgs/doc-reports-list.png deleted file mode 100644 index 4058ac04..00000000 Binary files a/docs/imgs/doc-reports-list.png and /dev/null differ diff --git a/docs/imgs/doc-reports-list.webp b/docs/imgs/doc-reports-list.webp new file mode 100644 index 00000000..630b3718 Binary files /dev/null and b/docs/imgs/doc-reports-list.webp differ diff --git a/docs/imgs/doc-reports-open.png b/docs/imgs/doc-reports-open.png deleted file mode 100644 index 1551a5f4..00000000 Binary files a/docs/imgs/doc-reports-open.png and /dev/null differ diff --git a/docs/imgs/doc-reports-open.webp b/docs/imgs/doc-reports-open.webp new file mode 100644 index 00000000..49fe5b2c Binary files /dev/null and b/docs/imgs/doc-reports-open.webp differ diff --git a/docs/imgs/doc-roles.png b/docs/imgs/doc-roles.png deleted file mode 100644 index b948ba2e..00000000 Binary files a/docs/imgs/doc-roles.png and /dev/null differ diff --git a/docs/imgs/doc-roles.webp b/docs/imgs/doc-roles.webp new file mode 100644 index 00000000..dcf261d9 Binary files /dev/null and b/docs/imgs/doc-roles.webp differ diff --git a/docs/imgs/doc-routines.png b/docs/imgs/doc-routines.png deleted file mode 100644 index 8ede326c..00000000 Binary files a/docs/imgs/doc-routines.png and /dev/null differ diff --git a/docs/imgs/doc-routines.webp b/docs/imgs/doc-routines.webp new file mode 100644 index 00000000..6327c380 Binary files /dev/null and b/docs/imgs/doc-routines.webp differ diff --git a/docs/imgs/doc-services.png b/docs/imgs/doc-services.png deleted file mode 100644 index 2619f51b..00000000 Binary files a/docs/imgs/doc-services.png and /dev/null differ diff --git a/docs/imgs/doc-services.webp b/docs/imgs/doc-services.webp new file mode 100644 index 00000000..eb25fa0a Binary files /dev/null and b/docs/imgs/doc-services.webp differ diff --git a/docs/imgs/doc-skills.png b/docs/imgs/doc-skills.png deleted file mode 100644 index 96fa4796..00000000 Binary files a/docs/imgs/doc-skills.png and /dev/null differ diff --git a/docs/imgs/doc-skills.webp b/docs/imgs/doc-skills.webp new file mode 100644 index 00000000..5736edee Binary files /dev/null and b/docs/imgs/doc-skills.webp differ diff --git a/docs/imgs/doc-systems-app.png b/docs/imgs/doc-systems-app.png deleted file mode 100644 index 9f1a611d..00000000 Binary files a/docs/imgs/doc-systems-app.png and /dev/null differ diff --git a/docs/imgs/doc-systems-app.webp b/docs/imgs/doc-systems-app.webp new file mode 100644 index 00000000..8ce90829 Binary files /dev/null and b/docs/imgs/doc-systems-app.webp differ diff --git a/docs/imgs/doc-systems-list.png b/docs/imgs/doc-systems-list.png deleted file mode 100644 index e03d8656..00000000 Binary files a/docs/imgs/doc-systems-list.png and /dev/null differ diff --git a/docs/imgs/doc-systems-list.webp b/docs/imgs/doc-systems-list.webp new file mode 100644 index 00000000..00aebe5d Binary files /dev/null and b/docs/imgs/doc-systems-list.webp differ diff --git a/docs/imgs/doc-tasks.png b/docs/imgs/doc-tasks.png deleted file mode 100644 index ce1a4271..00000000 Binary files a/docs/imgs/doc-tasks.png and /dev/null differ diff --git a/docs/imgs/doc-tasks.webp b/docs/imgs/doc-tasks.webp new file mode 100644 index 00000000..32aed7b1 Binary files /dev/null and b/docs/imgs/doc-tasks.webp differ diff --git a/docs/imgs/doc-templates.png b/docs/imgs/doc-templates.png deleted file mode 100644 index b237e3b7..00000000 Binary files a/docs/imgs/doc-templates.png and /dev/null differ diff --git a/docs/imgs/doc-templates.webp b/docs/imgs/doc-templates.webp new file mode 100644 index 00000000..dfb905d2 Binary files /dev/null and b/docs/imgs/doc-templates.webp differ diff --git a/docs/imgs/doc-triggers.png b/docs/imgs/doc-triggers.png deleted file mode 100644 index b14c8258..00000000 Binary files a/docs/imgs/doc-triggers.png and /dev/null differ diff --git a/docs/imgs/doc-triggers.webp b/docs/imgs/doc-triggers.webp new file mode 100644 index 00000000..a7d36d7b Binary files /dev/null and b/docs/imgs/doc-triggers.webp differ diff --git a/docs/imgs/doc-users.png b/docs/imgs/doc-users.png deleted file mode 100644 index f9bc7bc1..00000000 Binary files a/docs/imgs/doc-users.png and /dev/null differ diff --git a/docs/imgs/doc-users.webp b/docs/imgs/doc-users.webp new file mode 100644 index 00000000..65315cb5 Binary files /dev/null and b/docs/imgs/doc-users.webp differ diff --git a/docs/imgs/doc-workspace.png b/docs/imgs/doc-workspace.png deleted file mode 100644 index f806e388..00000000 Binary files a/docs/imgs/doc-workspace.png and /dev/null differ diff --git a/docs/imgs/doc-workspace.webp b/docs/imgs/doc-workspace.webp new file mode 100644 index 00000000..c95cb997 Binary files /dev/null and b/docs/imgs/doc-workspace.webp differ diff --git a/docs/integrations/overview.md b/docs/integrations/overview.md index 6d48727b..a16a729a 100644 --- a/docs/integrations/overview.md +++ b/docs/integrations/overview.md @@ -2,7 +2,7 @@ EvoNexus connects to external services through three mechanisms: **MCP servers**, **API clients**, and **OAuth flows**. Each integration provides data to one or more agents and routines. -![Integrations overview](../imgs/doc-integrations.png) +![Integrations overview](../imgs/doc-integrations.webp) ## Channels (Bidirectional) diff --git a/docs/introduction.md b/docs/introduction.md index 4f181625..3fa8f30d 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -78,7 +78,7 @@ Automated workflows (ADWs) that run on schedule via a Python scheduler. Morning A web UI (React + Flask) for managing everything: view reports, start/stop services, browse agents and skills, manage users and roles, and interact with Claude Code through an embedded terminal. -![Web Dashboard](imgs/doc-overview.png) +![Web Dashboard](imgs/doc-overview.webp) ### Knowledge Base diff --git a/public/cover.png b/public/cover.png deleted file mode 100644 index 5888d8ea..00000000 Binary files a/public/cover.png and /dev/null differ diff --git a/public/cover.webp b/public/cover.webp new file mode 100644 index 00000000..079448c3 Binary files /dev/null and b/public/cover.webp differ diff --git a/public/print-agents.png b/public/print-agents.png deleted file mode 100644 index f4669123..00000000 Binary files a/public/print-agents.png and /dev/null differ diff --git a/public/print-agents.webp b/public/print-agents.webp new file mode 100644 index 00000000..3ba40cdb Binary files /dev/null and b/public/print-agents.webp differ diff --git a/public/print-costs.png b/public/print-costs.png deleted file mode 100644 index fc214cab..00000000 Binary files a/public/print-costs.png and /dev/null differ diff --git a/public/print-costs.webp b/public/print-costs.webp new file mode 100644 index 00000000..680b0d6c Binary files /dev/null and b/public/print-costs.webp differ diff --git a/public/print-integrations.png b/public/print-integrations.png deleted file mode 100644 index 55bc3454..00000000 Binary files a/public/print-integrations.png and /dev/null differ diff --git a/public/print-integrations.webp b/public/print-integrations.webp new file mode 100644 index 00000000..433298c1 Binary files /dev/null and b/public/print-integrations.webp differ diff --git a/public/print-overview.png b/public/print-overview.png deleted file mode 100644 index 7e1bb034..00000000 Binary files a/public/print-overview.png and /dev/null differ diff --git a/public/print-overview.webp b/public/print-overview.webp new file mode 100644 index 00000000..c168a739 Binary files /dev/null and b/public/print-overview.webp differ diff --git a/setup.py b/setup.py index 74068678..81edbe73 100644 --- a/setup.py +++ b/setup.py @@ -31,82 +31,295 @@ def banner(): """) -def check_prerequisites(): - """Check that required tools are installed before proceeding.""" - errors = [] - - # Claude Code CLI +def _check_tool(name, cmd, install_cmd=None, install_label=None): + """Check if a tool is installed. If not, offer to install it.""" try: - result = subprocess.run(["claude", "--version"], capture_output=True, text=True, timeout=5) + result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) if result.returncode == 0: - version = result.stdout.strip() - print(f" {GREEN}✓{RESET} Claude Code CLI: {DIM}{version}{RESET}") - else: - errors.append("claude") + version = result.stdout.strip() or result.stderr.strip() + print(f" {GREEN}✓{RESET} {name}: {DIM}{version}{RESET}") + return True except (FileNotFoundError, subprocess.TimeoutExpired): - errors.append("claude") - - # Python / uv - try: - result = subprocess.run(["uv", "--version"], capture_output=True, text=True, timeout=5) - if result.returncode == 0: - version = result.stdout.strip() - print(f" {GREEN}✓{RESET} uv: {DIM}{version}{RESET}") + pass + + if install_cmd: + print(f" {YELLOW}!{RESET} {name} not found") + choice = input(f" Install {name}? (Y/n): ").strip().lower() + if choice in ("", "y", "yes", "s", "sim"): + print(f" {DIM}Installing {name}...{RESET}", end="", flush=True) + ret = os.system(f"{install_cmd} > /dev/null 2>&1") + # Re-check after install + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=10) + if result.returncode == 0: + version = result.stdout.strip() or result.stderr.strip() + print(f"\r {GREEN}✓{RESET} {name}: {DIM}{version}{RESET} ") + return True + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + print(f"\r {RED}✗{RESET} Failed to install {name} ") else: - errors.append("uv") - except (FileNotFoundError, subprocess.TimeoutExpired): - errors.append("uv") + print(f" {RED}✗{RESET} {name} is required for EvoNexus") + else: + print(f" {RED}✗{RESET} {name} not found — {install_label or 'install manually'}") - # Node.js + return False + + +def check_prerequisites(): + """Check and auto-install required tools.""" + # Update system packages first (ensures fresh package lists) + if os.getuid() == 0: + print(f" {DIM}Updating system packages...{RESET}", end="", flush=True) + os.system("DEBIAN_FRONTEND=noninteractive apt-get update -y -qq > /dev/null 2>&1") + os.system("DEBIAN_FRONTEND=noninteractive apt-get upgrade -y -qq -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' > /dev/null 2>&1") + print(f"\r {GREEN}✓{RESET} System packages updated ") + + missing = [] + + # build-essential (required for native npm packages like node-pty) try: - result = subprocess.run(["node", "--version"], capture_output=True, text=True, timeout=5) + result = subprocess.run(["g++", "--version"], capture_output=True, text=True, timeout=5) if result.returncode == 0: - version = result.stdout.strip() - print(f" {GREEN}✓{RESET} Node.js: {DIM}{version}{RESET}") + print(f" {GREEN}✓{RESET} build-essential: {DIM}installed{RESET}") else: - errors.append("node") - except (FileNotFoundError, subprocess.TimeoutExpired): - errors.append("node") - - # npm - npm_cmd = "npm" - try: - result = subprocess.run([npm_cmd, "--version"], capture_output=True, text=True, timeout=5) - if result.returncode != 0: raise FileNotFoundError - print(f" {GREEN}✓{RESET} npm: {DIM}v{result.stdout.strip()}{RESET}") except (FileNotFoundError, subprocess.TimeoutExpired): + print(f" {DIM}Installing build-essential...{RESET}", end="", flush=True) + os.system("apt install -y build-essential > /dev/null 2>&1 || yum groupinstall -y 'Development Tools' > /dev/null 2>&1") try: - npm_cmd = "npm.cmd" - result = subprocess.run([npm_cmd, "--version"], capture_output=True, text=True, timeout=5) + result = subprocess.run(["g++", "--version"], capture_output=True, text=True, timeout=5) if result.returncode == 0: - print(f" {GREEN}✓{RESET} npm: {DIM}v{result.stdout.strip()}{RESET}") + print(f" {GREEN}✓{RESET} build-essential: {DIM}installed{RESET}") else: - errors.append("npm") + print(f" {RED}✗{RESET} build-essential install failed") + missing.append("build-essential") + except (FileNotFoundError, subprocess.TimeoutExpired): + print(f" {RED}✗{RESET} build-essential install failed") + missing.append("build-essential") + + # Node.js + if not _check_tool("Node.js", ["node", "--version"], + install_cmd="curl -fsSL https://deb.nodesource.com/setup_24.x | bash - && apt install -y nodejs 2>/dev/null || echo 'Install Node.js 18+ from https://nodejs.org'", + install_label="https://nodejs.org"): + missing.append("node") + + # npm (comes with Node.js) + npm_ok = False + for cmd in ["npm", "npm.cmd"]: + try: + result = subprocess.run([cmd, "--version"], capture_output=True, text=True, timeout=5) + if result.returncode == 0: + print(f" {GREEN}✓{RESET} npm: {DIM}v{result.stdout.strip()}{RESET}") + npm_ok = True + break except (FileNotFoundError, subprocess.TimeoutExpired): - errors.append("npm") + continue + if not npm_ok: + print(f" {RED}✗{RESET} npm not found (should come with Node.js)") + missing.append("npm") + + # uv (Python package manager) + # When running with sudo, install for the original user and add their + # ~/.local/bin to root's PATH BEFORE verification + _sudo_user_uv = os.environ.get("SUDO_USER", "") + if _sudo_user_uv and os.getuid() == 0: + # Resolve user home FIRST so we can find uv after install + try: + user_home = subprocess.run(["getent", "passwd", _sudo_user_uv], capture_output=True, text=True).stdout.split(":")[5] + except (IndexError, Exception): + user_home = f"/home/{_sudo_user_uv}" + user_uv_bin = os.path.join(user_home, ".local", "bin") + # Add user's bin to PATH before any uv checks + if user_uv_bin not in os.environ.get("PATH", ""): + os.environ["PATH"] = f"{user_uv_bin}:{os.environ.get('PATH', '')}" + # Now check/install + if not _check_tool("uv", ["uv", "--version"], + install_cmd=f"su - {_sudo_user_uv} -c 'curl -LsSf https://astral.sh/uv/install.sh | sh'"): + missing.append("uv") + else: + home_bin = os.path.join(os.path.expanduser("~"), ".local", "bin") + if home_bin not in os.environ.get("PATH", ""): + os.environ["PATH"] = f"{home_bin}:{os.environ.get('PATH', '')}" + if not _check_tool("uv", ["uv", "--version"], + install_cmd="curl -LsSf https://astral.sh/uv/install.sh | sh"): + missing.append("uv") + + # Claude Code CLI + if not _check_tool("Claude Code CLI", ["claude", "--version"], + install_cmd="npm install -g @anthropic-ai/claude-code"): + missing.append("claude") + + # OpenClaude (required for non-Anthropic providers) + if not _check_tool("OpenClaude", ["openclaude", "--version"], + install_cmd="npm install -g @gitlawb/openclaude"): + missing.append("openclaude") print() - if errors: - print(f" {RED}✗ Missing required tools:{RESET}") - if "claude" in errors: - print(f" {RED}•{RESET} Claude Code CLI — install from {BOLD}https://claude.ai/download{RESET}") - print(f" {DIM}npm install -g @anthropic-ai/claude-code{RESET}") - if "uv" in errors: - print(f" {RED}•{RESET} uv (Python package manager) — {BOLD}https://docs.astral.sh/uv/{RESET}") - print(f" {DIM}curl -LsSf https://astral.sh/uv/install.sh | sh{RESET}") - if "node" in errors: - print(f" {RED}•{RESET} Node.js 18+ — {BOLD}https://nodejs.org{RESET}") - if "npm" in errors: - print(f" {RED}•{RESET} npm not found (Node.js installed but npm missing from PATH) — {BOLD}https://nodejs.org{RESET}") - print() - print(f" {YELLOW}Install the missing tools and run setup again.{RESET}") + if missing: + print(f" {RED}The following tools could not be installed:{RESET}") + for m in missing: + print(f" {RED}•{RESET} {m}") + print(f"\n {YELLOW}Install them manually and run setup again.{RESET}") sys.exit(1) return True +def configure_access() -> dict: + """Configure how the dashboard will be accessed (local or domain with SSL).""" + print(f"\n {BOLD}Dashboard Access{RESET}\n") + print(f" {BOLD}1{RESET}) Local only (http://localhost:8080)") + print(f" {BOLD}2{RESET}) Domain with SSL (recommended for remote servers)") + + choice = ask("Choice", "1") + if choice != "2": + return {"mode": "local", "url": "http://localhost:8080"} + + domain = ask("Domain", "") + if not domain: + print(f" {YELLOW}!{RESET} No domain provided, using local mode") + return {"mode": "local", "url": "http://localhost:8080"} + + # Step 1: Install nginx + if not shutil.which("nginx"): + print(f" {DIM}Installing nginx...{RESET}", end="", flush=True) + os.system("apt install -y nginx > /dev/null 2>&1 || yum install -y nginx > /dev/null 2>&1") + if not shutil.which("nginx"): + print(f" {RED}✗{RESET} nginx installation failed, using local mode") + return {"mode": "local", "url": "http://localhost:8080"} + print(f" {GREEN}✓{RESET} nginx installed") + + # Step 2: Stop nginx to free port 80 for certbot + os.system("systemctl stop nginx 2>/dev/null") + + # Step 3: SSL certificate — certbot by default, fallback to self-signed + ssl_cert = "" + ssl_key = "" + + ssl_mode = ask("SSL certificate (1=certbot, 2=self-signed, 3=manual path)", "1") + + if ssl_mode == "1": + certbot_cert = f"/etc/letsencrypt/live/{domain}/fullchain.pem" + certbot_key = f"/etc/letsencrypt/live/{domain}/privkey.pem" + + # Reuse existing certbot cert if found + if os.path.isfile(certbot_cert) and os.path.isfile(certbot_key): + ssl_cert = certbot_cert + ssl_key = certbot_key + print(f" {GREEN}✓{RESET} Existing certbot certificate found for {domain}") + else: + # Install certbot if needed + if not shutil.which("certbot"): + print(f" {DIM}Installing certbot...{RESET}", end="", flush=True) + os.system("apt install -y certbot > /dev/null 2>&1") + print(f"\r {GREEN}✓{RESET} certbot installed ") + # Obtain certificate (requires domain DNS pointing to this server) + print(f" {DIM}Obtaining SSL certificate via certbot...{RESET}", end="", flush=True) + ret = os.system(f"certbot certonly --standalone -d {domain} --non-interactive --agree-tos --register-unsafely-without-email > /dev/null 2>&1") + if ret == 0: + ssl_cert = certbot_cert + ssl_key = certbot_key + print(f"\r {GREEN}✓{RESET} SSL certificate obtained via certbot ") + else: + print(f"\r {YELLOW}!{RESET} certbot failed — falling back to self-signed ") + ssl_mode = "2" + + if ssl_mode == "2": + # Self-signed (works with Cloudflare Full mode) + print(f" {DIM}Generating self-signed SSL certificate...{RESET}") + os.system("mkdir -p /etc/nginx/ssl") + ret = os.system(f'openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout /etc/nginx/ssl/{domain}.key -out /etc/nginx/ssl/{domain}.crt -subj "/CN={domain}" 2>/dev/null') + if ret == 0: + ssl_cert = f"/etc/nginx/ssl/{domain}.crt" + ssl_key = f"/etc/nginx/ssl/{domain}.key" + print(f" {GREEN}✓{RESET} Self-signed SSL certificate generated") + print(f" {DIM}(Compatible with Cloudflare SSL mode: Full){RESET}") + else: + print(f" {RED}✗{RESET} Failed to generate SSL certificate") + + if ssl_mode == "3": + ssl_cert = ask("SSL cert path", f"/etc/nginx/ssl/{domain}.crt") + ssl_key = ask("SSL key path", f"/etc/nginx/ssl/{domain}.key") + + # Fix SSL key permissions (nginx needs read access, restrict from others) + if ssl_key and os.path.isfile(ssl_key): + os.chmod(ssl_key, 0o600) + + if not ssl_cert or not ssl_key: + print(f" {RED}✗{RESET} No SSL certificate available, using local mode") + os.system("systemctl start nginx 2>/dev/null") + return {"mode": "local", "url": "http://localhost:8080"} + + # Step 4: Write Nginx config with IPv6 support + nginx_config = f"""server {{ + listen 80; + listen [::]:80; + server_name {domain}; + return 301 https://$host$request_uri; +}} + +server {{ + listen 443 ssl; + listen [::]:443 ssl; + server_name {domain}; + + ssl_certificate {ssl_cert}; + ssl_certificate_key {ssl_key}; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + location / {{ + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 86400; + }} + + location /terminal/ {{ + proxy_pass http://127.0.0.1:32352/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 86400; + }} +}} +""" + try: + # Remove default nginx site if exists + if os.path.exists("/etc/nginx/sites-enabled/default"): + os.remove("/etc/nginx/sites-enabled/default") + + nginx_path = "/etc/nginx/sites-enabled/evonexus" + with open(nginx_path, "w") as f: + f.write(nginx_config) + ret = os.system("nginx -t 2>/dev/null && systemctl start nginx 2>/dev/null && systemctl enable nginx 2>/dev/null") + if ret == 0: + print(f" {GREEN}✓{RESET} Nginx configured for {domain}") + else: + print(f" {YELLOW}!{RESET} Nginx config written but start failed — check manually") + except PermissionError: + print(f" {YELLOW}!{RESET} No permission to write nginx config — run setup as root/sudo") + + # Step 5: Open firewall ports + print(f" {DIM}Configuring firewall...{RESET}") + os.system("ufw allow 80/tcp 2>/dev/null; ufw allow 443/tcp 2>/dev/null; ufw allow 8080/tcp 2>/dev/null; ufw allow 32352/tcp 2>/dev/null") + os.system("iptables -I INPUT -p tcp --dport 80 -j ACCEPT 2>/dev/null; iptables -I INPUT -p tcp --dport 443 -j ACCEPT 2>/dev/null") + print(f" {GREEN}✓{RESET} Firewall ports opened (80, 443)") + + return {"mode": "domain", "url": f"https://{domain}"} + + def choose_provider() -> str: """Ask the user which AI provider to use.""" print(f""" @@ -114,17 +327,18 @@ def choose_provider() -> str: {BOLD}1{RESET}) Anthropic (Claude nativo) — default, no extra config {BOLD}2{RESET}) OpenRouter (200+ models) — requires API key + openclaude - {BOLD}3{RESET}) OpenAI (GPT-4o, GPT-4.1, o3) — requires API key + openclaude - {BOLD}4{RESET}) Google Gemini — requires API key + openclaude - {BOLD}5{RESET}) Codex Auth (OpenAI via OAuth) — requires codex login + openclaude - {BOLD}6{RESET}) AWS Bedrock — requires AWS creds + openclaude - {BOLD}7{RESET}) Google Vertex AI — requires GCP creds + openclaude + {BOLD}3{RESET}) OpenAI (GPT-4.x / GPT-5.x) — API key or OAuth + openclaude + {BOLD}4{RESET}) Google Gemini — coming soon + {BOLD}5{RESET}) AWS Bedrock — coming soon + {BOLD}6{RESET}) Google Vertex AI — coming soon """) - choice = ask("Provider (1-7)", "1") + choice = ask("Provider (1-3)", "1") provider_map = { "1": "anthropic", "2": "openrouter", "3": "openai", - "4": "gemini", "5": "codex_auth", "6": "bedrock", "7": "vertex", } + if choice in ("4", "5", "6"): + print(f" {YELLOW}!{RESET} This provider is coming soon. Using Anthropic for now.") + choice = "1" provider_id = provider_map.get(choice, "anthropic") # Check if openclaude is needed @@ -154,11 +368,10 @@ def choose_provider() -> str: "providers": { "anthropic": {"name": "Anthropic (Claude nativo)", "cli_command": "claude", "env_vars": {}, "requires_logout": False}, "openrouter": {"name": "OpenRouter", "cli_command": "openclaude", "env_vars": {"CLAUDE_CODE_USE_OPENAI": "1", "OPENAI_BASE_URL": "", "OPENAI_API_KEY": "", "OPENAI_MODEL": ""}, "default_base_url": "https://openrouter.ai/api/v1", "default_model": "anthropic/claude-sonnet-4", "requires_logout": True}, - "openai": {"name": "OpenAI", "cli_command": "openclaude", "env_vars": {"CLAUDE_CODE_USE_OPENAI": "1", "OPENAI_API_KEY": "", "OPENAI_MODEL": ""}, "default_model": "gpt-4.1", "requires_logout": True}, - "gemini": {"name": "Google Gemini", "cli_command": "openclaude", "env_vars": {"CLAUDE_CODE_USE_GEMINI": "1", "GEMINI_API_KEY": "", "GEMINI_MODEL": ""}, "default_model": "gemini-2.5-pro", "requires_logout": True}, - "codex_auth": {"name": "Codex Auth", "cli_command": "openclaude", "env_vars": {"CLAUDE_CODE_USE_OPENAI": "1", "OPENAI_API_KEY": ""}, "requires_logout": True, "setup_hint": "Run 'codex login' first"}, - "bedrock": {"name": "AWS Bedrock", "cli_command": "openclaude", "env_vars": {"CLAUDE_CODE_USE_BEDROCK": "1", "AWS_REGION": "", "AWS_BEARER_TOKEN_BEDROCK": ""}, "requires_logout": True}, - "vertex": {"name": "Google Vertex AI", "cli_command": "openclaude", "env_vars": {"CLAUDE_CODE_USE_VERTEX": "1", "ANTHROPIC_VERTEX_PROJECT_ID": "", "CLOUD_ML_REGION": ""}, "default_region": "us-east5", "requires_logout": True}, + "openai": {"name": "OpenAI", "description": "GPT-4.x via API Key ou GPT-5.x via Codex OAuth", "cli_command": "openclaude", "env_vars": {"CLAUDE_CODE_USE_OPENAI": "1", "OPENAI_API_KEY": "", "OPENAI_MODEL": ""}, "default_model": "gpt-4.1", "requires_logout": True}, + "gemini": {"name": "Google Gemini (em breve)", "cli_command": "openclaude", "env_vars": {"CLAUDE_CODE_USE_GEMINI": "1", "GEMINI_API_KEY": "", "GEMINI_MODEL": ""}, "default_model": "gemini-2.5-pro", "requires_logout": True, "coming_soon": True}, + "bedrock": {"name": "AWS Bedrock (em breve)", "cli_command": "openclaude", "env_vars": {"CLAUDE_CODE_USE_BEDROCK": "1", "AWS_REGION": "", "AWS_BEARER_TOKEN_BEDROCK": ""}, "requires_logout": True, "coming_soon": True}, + "vertex": {"name": "Google Vertex AI (em breve)", "cli_command": "openclaude", "env_vars": {"CLAUDE_CODE_USE_VERTEX": "1", "ANTHROPIC_VERTEX_PROJECT_ID": "", "CLOUD_ML_REGION": ""}, "default_region": "us-east5", "requires_logout": True, "coming_soon": True}, } } @@ -166,11 +379,30 @@ def choose_provider() -> str: prov = config["providers"].get(provider_id, {}) env_vars = prov.get("env_vars", {}) - if provider_id != "anthropic": + if provider_id == "openai": + print(f"\n {BOLD}OpenAI Authentication{RESET}") + print(f" {BOLD}a{RESET}) API Key (GPT-4.x)") + print(f" {BOLD}b{RESET}) Codex OAuth (GPT-5.x) — via Dashboard") + auth_choice = ask("Auth method (a/b)", "b") + + if auth_choice.lower() == "a": + api_key = ask(" OPENAI_API_KEY", "") + model = ask(" OPENAI_MODEL", prov.get("default_model", "gpt-4.1")) + env_vars = {"CLAUDE_CODE_USE_OPENAI": "1", "OPENAI_API_KEY": api_key, "OPENAI_MODEL": model} + else: + model = ask(" OPENAI_MODEL", "gpt-5.4") + env_vars = {"CLAUDE_CODE_USE_OPENAI": "1", "OPENAI_MODEL": model} + print(f"\n {GREEN}✓{RESET} Provider configurado: OpenAI (Codex OAuth)") + print(f" {YELLOW}!{RESET} Para completar a autenticacao, acesse o Dashboard") + print(f" {BOLD}Providers → Login com OpenAI{RESET}") + + prov["env_vars"] = env_vars + + elif provider_id != "anthropic": print(f"\n {BOLD}Configure {prov.get('name', provider_id)}{RESET}") for key, current in env_vars.items(): if key.startswith("CLAUDE_CODE_USE_"): - continue # Flags are automatic + continue default = prov.get("default_base_url", "") if key == "OPENAI_BASE_URL" else prov.get("default_model", "") if "MODEL" in key else prov.get("default_region", "") if "REGION" in key else current value = ask(f" {key}", default) env_vars[key] = value @@ -311,7 +543,7 @@ def generate_claude_md(config: dict) -> str: This workspace exists to produce things, not just store them. Everything here is oriented around a loop: **define a goal → break it into problems → solve those problems → deliver the output.** -Claude's role is to keep {config['owner_name'].split()[0]} moving through this loop. If there's no goal yet, help define one. If there's a goal but no clear problems, help break it down. If there are problems, help solve the next one. Always push toward the next concrete thing to do or deliver. +Claude's role is to keep {(config['owner_name'].split()[0] if config['owner_name'].strip() else 'the user')} moving through this loop. If there's no goal yet, help define one. If there's a goal but no clear problems, help break it down. If there are problems, help solve the next one. Always push toward the next concrete thing to do or deliver. --- @@ -469,19 +701,33 @@ def main(): print(f" {BOLD}Checking prerequisites...{RESET}") check_prerequisites() - # Provider choice - print(f" {BOLD}AI Provider{RESET}") - provider_choice = choose_provider() - print() + # Dashboard access (Nginx config) — FIRST question + access_config = configure_access() + is_remote = access_config.get("mode") == "domain" + + if is_remote: + # Remote mode: minimal setup, then redirect to dashboard + print(f"\n {BOLD}Quick setup for remote access...{RESET}") + owner_name = "" + company_name = "" + timezone = "America/Sao_Paulo" + language = "ptBR" + dashboard_port = 8080 + else: + # Local mode: full interactive setup + # Provider choice + print(f" {BOLD}AI Provider{RESET}") + provider_choice = choose_provider() + print() - # Who are you? - print(f" {BOLD}About you{RESET}") - owner_name = ask("Your name", "") - company_name = ask("Company name", "") - timezone = ask("Timezone", "America/Sao_Paulo") - language = ask("Language", "en") - dashboard_port = int(ask("Dashboard port", "8080")) - print() + # Who are you? + print(f" {BOLD}About you{RESET}") + owner_name = ask("Your name", "") + company_name = ask("Company name", "") + timezone = ask("Timezone", "America/Sao_Paulo") + language = ask("Language", "ptBR") + dashboard_port = int(ask("Dashboard port", "8080")) + print() # All agents and integrations enabled by default agents = [a["key"] for a in AGENTS] @@ -525,31 +771,133 @@ def main(): create_folders(config) # Install Python dependencies + # Must run as the ORIGINAL user (not root) so .venv symlinks + # point to user's Python, not /root/.local/share/uv/python/ print(f" {DIM}Installing Python dependencies...{RESET}") - os.system(f"cd {WORKSPACE} && uv sync -q 2>/dev/null") + _sudo_user = os.environ.get("SUDO_USER", "") + if _sudo_user and os.getuid() == 0: + os.system(f"su - {_sudo_user} -c 'cd {WORKSPACE} && uv sync -q'") + else: + os.system(f"cd {WORKSPACE} && uv sync -q") print(f" {GREEN}✓{RESET} Installed Python dependencies") # Dashboard build frontend_dir = WORKSPACE / "dashboard" / "frontend" if (frontend_dir / "package.json").exists(): - print(f" {DIM}Building dashboard frontend...{RESET}") - os.system(f"cd {frontend_dir} && npm install --silent && npm run build --silent 2>/dev/null") - print(f" {GREEN}✓{RESET} Built dashboard frontend") + print(f" {DIM}Building dashboard frontend...{RESET}", end="", flush=True) + os.system(f"cd {frontend_dir} && npm install --silent > /dev/null 2>&1 && npm run build --silent > /dev/null 2>&1") + print(f"\r {GREEN}✓{RESET} Built dashboard frontend ") + + # Terminal-server dependencies (always needed) + ts_dir = WORKSPACE / "dashboard" / "terminal-server" + if (ts_dir / "package.json").exists(): + print(f" {DIM}Installing terminal-server dependencies...{RESET}", end="", flush=True) + os.system(f"cd {ts_dir} && npm install --silent > /dev/null 2>&1") + print(f"\r {GREEN}✓{RESET} Installed terminal-server dependencies ") # Data dir for SQLite (WORKSPACE / "dashboard" / "data").mkdir(parents=True, exist_ok=True) - print(f""" + # Fix ownership BEFORE starting services. + # When running with sudo, all files (including .venv, node_modules, + # frontend dist, data dir) are created as root. The services MUST + # run as the original user, so we chown everything now. + sudo_user = os.environ.get("SUDO_USER", "") + if sudo_user and os.getuid() == 0: + print(f" {DIM}Fixing file ownership for {sudo_user}...{RESET}") + os.system(f"chown -R {sudo_user}:{sudo_user} {WORKSPACE}") + # Ensure .venv binaries are executable after chown + os.system(f"chmod -R u+x {WORKSPACE}/.venv/bin/ 2>/dev/null") + run_as = f"su - {sudo_user} -c" + print(f" {GREEN}✓{RESET} Ownership fixed") + else: + run_as = "bash -c" + + # Start dashboard services + logs_dir = WORKSPACE / "logs" + logs_dir.mkdir(exist_ok=True) + if sudo_user and os.getuid() == 0: + os.system(f"chown -R {sudo_user}:{sudo_user} {logs_dir}") + print(f"\n {DIM}Starting dashboard services...{RESET}") + # Stop any existing services (systemd, background processes) + os.system("systemctl stop evonexus 2>/dev/null; systemctl disable evonexus 2>/dev/null") + os.system("pkill -f 'terminal-server/bin/server.js' 2>/dev/null") + os.system("pkill -f 'app.py' 2>/dev/null") + os.system("sleep 1") + if sudo_user: + print(f" {DIM}(services will run as {sudo_user}, not root){RESET}") + + # Start terminal-server + # Create a startup script that persists processes properly + startup_script = WORKSPACE / "start-services.sh" + startup_script.write_text(f"""#!/bin/bash +export PATH="/usr/local/bin:/usr/bin:/bin:$HOME/.local/bin" +cd {WORKSPACE} + +# Kill existing services +pkill -f 'terminal-server/bin/server.js' 2>/dev/null +pkill -f 'dashboard/backend.*app.py' 2>/dev/null +sleep 1 + +# Clean stale sessions — old sessions cause agent persona issues +rm -f $HOME/.claude-code-web/sessions.json 2>/dev/null + +# Start terminal-server (must run FROM the project root for agent discovery) +nohup node dashboard/terminal-server/bin/server.js > {logs_dir}/terminal-server.log 2>&1 & + +# Start Flask dashboard +cd dashboard/backend +nohup {WORKSPACE}/.venv/bin/python app.py > {logs_dir}/dashboard.log 2>&1 & +""", encoding="utf-8") + os.chmod(startup_script, 0o755) + + if sudo_user: + os.system(f"su - {sudo_user} -c '{startup_script}'") + else: + os.system(str(startup_script)) + import time as _time + _time.sleep(3) + # Verify + import urllib.request as _urllib + try: + _urllib.urlopen("http://localhost:32352", timeout=3) + print(f" {GREEN}✓{RESET} Terminal server started (port 32352)") + except Exception: + print(f" {YELLOW}!{RESET} Terminal server may not have started — check logs/terminal-server.log") + try: + _urllib.urlopen("http://localhost:8080", timeout=3) + print(f" {GREEN}✓{RESET} Dashboard started (port 8080)") + except Exception: + print(f" {YELLOW}!{RESET} Dashboard may not have started — check logs/dashboard.log") + + dashboard_url = access_config.get('url', f'http://localhost:{dashboard_port}') + + if is_remote: + print(f""" + {GREEN}{'='*50}{RESET} + {GREEN}Setup concluido!{RESET} + {GREEN}{'='*50}{RESET} + + Dashboard disponivel em: + + {BOLD}{dashboard_url}{RESET} + + Proximo passo: + 1. Acesse o link acima e crie sua conta de administrador + 2. Va em {BOLD}Providers{RESET} e configure o AI Provider + 3. Abra um agente e comece a usar! +""") + else: + print(f""" {GREEN}Done!{RESET} Next steps: 1. Edit {BOLD}.env{RESET} with your API keys 2. Run: {BOLD}make dashboard-app{RESET} - 3. Open {BOLD}http://localhost:{dashboard_port}{RESET} to create your admin account + 3. Open {BOLD}{dashboard_url}{RESET} to create your admin account 4. Run: {BOLD}make scheduler{RESET} — start automated routines 5. Run: {BOLD}make help{RESET} — see all commands {YELLOW}Note:{RESET} The admin account is created via the web dashboard, - not via CLI. This allows us to collect anonymous telemetry - (geo, version) for the open source project. + not via CLI. """) diff --git a/site/public/assets/EVO_NEXUS.png b/site/public/assets/EVO_NEXUS.png deleted file mode 100644 index 921f8216..00000000 Binary files a/site/public/assets/EVO_NEXUS.png and /dev/null differ diff --git a/site/public/assets/EVO_NEXUS.webp b/site/public/assets/EVO_NEXUS.webp new file mode 100644 index 00000000..f51a8f91 Binary files /dev/null and b/site/public/assets/EVO_NEXUS.webp differ diff --git a/site/public/assets/logo.png b/site/public/assets/logo.png deleted file mode 100644 index 6da3413c..00000000 Binary files a/site/public/assets/logo.png and /dev/null differ diff --git a/site/public/assets/logo.webp b/site/public/assets/logo.webp new file mode 100644 index 00000000..e7d175f7 Binary files /dev/null and b/site/public/assets/logo.webp differ diff --git a/site/public/assets/print-agents.png b/site/public/assets/print-agents.png deleted file mode 100644 index f4669123..00000000 Binary files a/site/public/assets/print-agents.png and /dev/null differ diff --git a/site/public/assets/print-agents.webp b/site/public/assets/print-agents.webp new file mode 100644 index 00000000..3ba40cdb Binary files /dev/null and b/site/public/assets/print-agents.webp differ diff --git a/site/public/assets/print-chat.png b/site/public/assets/print-chat.png deleted file mode 100644 index 68fd0212..00000000 Binary files a/site/public/assets/print-chat.png and /dev/null differ diff --git a/site/public/assets/print-chat.webp b/site/public/assets/print-chat.webp new file mode 100644 index 00000000..f99e0ab9 Binary files /dev/null and b/site/public/assets/print-chat.webp differ diff --git a/site/public/assets/print-costs.png b/site/public/assets/print-costs.png deleted file mode 100644 index fc214cab..00000000 Binary files a/site/public/assets/print-costs.png and /dev/null differ diff --git a/site/public/assets/print-costs.webp b/site/public/assets/print-costs.webp new file mode 100644 index 00000000..680b0d6c Binary files /dev/null and b/site/public/assets/print-costs.webp differ diff --git a/site/public/assets/print-integrations.png b/site/public/assets/print-integrations.png deleted file mode 100644 index 55bc3454..00000000 Binary files a/site/public/assets/print-integrations.png and /dev/null differ diff --git a/site/public/assets/print-integrations.webp b/site/public/assets/print-integrations.webp new file mode 100644 index 00000000..433298c1 Binary files /dev/null and b/site/public/assets/print-integrations.webp differ diff --git a/site/public/assets/print-overview.png b/site/public/assets/print-overview.png deleted file mode 100644 index 7e1bb034..00000000 Binary files a/site/public/assets/print-overview.png and /dev/null differ diff --git a/site/public/assets/print-overview.webp b/site/public/assets/print-overview.webp new file mode 100644 index 00000000..c168a739 Binary files /dev/null and b/site/public/assets/print-overview.webp differ diff --git a/site/src/pages/Home.tsx b/site/src/pages/Home.tsx index a18c350d..7eb73347 100644 --- a/site/src/pages/Home.tsx +++ b/site/src/pages/Home.tsx @@ -13,12 +13,12 @@ import { SiInstagram, SiCanva, SiNotion, SiObsidian } from "react-icons/si"; -import MainLogo from "@assets/logo.png"; -import EvoNexusLogo from "@assets/EVO_NEXUS.png"; -import printOverview from "@assets/print-overview.png"; -import printAgents from "@assets/print-agents.png"; -import printIntegrations from "@assets/print-integrations.png"; -import printCosts from "@assets/print-costs.png"; +import MainLogo from "@assets/logo.webp"; +import EvoNexusLogo from "@assets/EVO_NEXUS.webp"; +import printOverview from "@assets/print-overview.webp"; +import printAgents from "@assets/print-agents.webp"; +import printIntegrations from "@assets/print-integrations.webp"; +import printCosts from "@assets/print-costs.webp"; const FadeIn = ({ children, delay = 0, className = "" }: { children: React.ReactNode, delay?: number, className?: string }) => { const ref = useRef(null);