+
+ {/* 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 && (
-
)
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.
-
+
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.
-
+
## 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.
-
+
### 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.
-
+
-
+
### 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.
-
+
-
+
### 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.
-
+
### 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.
-
+
### 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.
-
+
### 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.
-
+
### 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.
-
+
### Templates
Preview the HTML report templates from `.claude/templates/html/`. See how each template renders before routines use them.
-
+
### 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.
-
+
### 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.
-
+
### 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/`
-
+
### 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.
-
+
### 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.
-
+
### 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.
-
+
### Users
User management page (admin only). Create, edit, deactivate users. Assign roles. See last login timestamps.
-
+
### 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.
-
+
### 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.
-
+
### 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.
-
+
### 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.
-
+
### Backups
@@ -179,7 +179,7 @@ Export and restore workspace data (all gitignored user files). Create local back
### Config
-
+
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.
-
+
### 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**
-
+
### 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.
-
+
### 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.
-
+
## 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.
-
+
### 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);