apteva 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/App.m4hg4bxq.js +218 -0
  2. package/dist/index.html +4 -2
  3. package/dist/styles.css +1 -1
  4. package/package.json +1 -1
  5. package/src/auth/index.ts +386 -0
  6. package/src/auth/middleware.ts +183 -0
  7. package/src/binary.ts +19 -1
  8. package/src/db.ts +688 -45
  9. package/src/integrations/composio.ts +437 -0
  10. package/src/integrations/index.ts +80 -0
  11. package/src/openapi.ts +1724 -0
  12. package/src/routes/api.ts +1476 -118
  13. package/src/routes/auth.ts +242 -0
  14. package/src/server.ts +121 -11
  15. package/src/web/App.tsx +64 -19
  16. package/src/web/components/agents/AgentCard.tsx +24 -22
  17. package/src/web/components/agents/AgentPanel.tsx +810 -45
  18. package/src/web/components/agents/AgentsView.tsx +81 -9
  19. package/src/web/components/agents/CreateAgentModal.tsx +28 -1
  20. package/src/web/components/api/ApiDocsPage.tsx +583 -0
  21. package/src/web/components/auth/CreateAccountStep.tsx +176 -0
  22. package/src/web/components/auth/LoginPage.tsx +91 -0
  23. package/src/web/components/auth/index.ts +2 -0
  24. package/src/web/components/common/Icons.tsx +56 -0
  25. package/src/web/components/common/Modal.tsx +184 -1
  26. package/src/web/components/dashboard/Dashboard.tsx +70 -22
  27. package/src/web/components/index.ts +3 -0
  28. package/src/web/components/layout/Header.tsx +135 -18
  29. package/src/web/components/layout/Sidebar.tsx +87 -43
  30. package/src/web/components/mcp/IntegrationsPanel.tsx +743 -0
  31. package/src/web/components/mcp/McpPage.tsx +451 -63
  32. package/src/web/components/onboarding/OnboardingWizard.tsx +64 -8
  33. package/src/web/components/settings/SettingsPage.tsx +340 -26
  34. package/src/web/components/tasks/TasksPage.tsx +22 -20
  35. package/src/web/components/telemetry/TelemetryPage.tsx +163 -61
  36. package/src/web/context/AuthContext.tsx +230 -0
  37. package/src/web/context/ProjectContext.tsx +182 -0
  38. package/src/web/context/index.ts +5 -0
  39. package/src/web/hooks/useAgents.ts +18 -6
  40. package/src/web/hooks/useOnboarding.ts +20 -4
  41. package/src/web/hooks/useProviders.ts +15 -5
  42. package/src/web/icon.png +0 -0
  43. package/src/web/index.html +1 -1
  44. package/src/web/styles.css +12 -0
  45. package/src/web/types.ts +10 -1
  46. package/dist/App.3kb50qa3.js +0 -213
@@ -0,0 +1,242 @@
1
+ import { UserDB } from "../db";
2
+ import {
3
+ login,
4
+ refreshSession,
5
+ invalidateSession,
6
+ getAuthStatus,
7
+ verifyAccessToken,
8
+ hashPassword,
9
+ validatePassword,
10
+ REFRESH_TOKEN_EXPIRY,
11
+ } from "../auth";
12
+ import {
13
+ getTokenFromRequest,
14
+ getRefreshTokenFromCookie,
15
+ createRefreshTokenCookie,
16
+ clearRefreshTokenCookie,
17
+ } from "../auth/middleware";
18
+
19
+ function json(data: unknown, status = 200, headers: Record<string, string> = {}): Response {
20
+ return new Response(JSON.stringify(data), {
21
+ status,
22
+ headers: { "Content-Type": "application/json", ...headers },
23
+ });
24
+ }
25
+
26
+ export async function handleAuthRequest(req: Request, path: string): Promise<Response> {
27
+ const method = req.method;
28
+
29
+ // GET /api/auth/check - Check authentication status
30
+ if (path === "/api/auth/check" && method === "GET") {
31
+ const token = getTokenFromRequest(req);
32
+ const status = getAuthStatus(token || undefined);
33
+ return json(status);
34
+ }
35
+
36
+ // POST /api/auth/login - Login with username and password
37
+ if (path === "/api/auth/login" && method === "POST") {
38
+ try {
39
+ const body = await req.json();
40
+ const { username, password } = body;
41
+
42
+ if (!username || !password) {
43
+ return json({ error: "Username and password are required" }, 400);
44
+ }
45
+
46
+ const result = await login(username, password);
47
+
48
+ if (!result.success) {
49
+ return json({ error: result.error }, 401);
50
+ }
51
+
52
+ // Set refresh token as httpOnly cookie
53
+ const cookieHeader = createRefreshTokenCookie(result.tokens!.refreshToken, REFRESH_TOKEN_EXPIRY);
54
+
55
+ return json(
56
+ {
57
+ user: result.user,
58
+ accessToken: result.tokens!.accessToken,
59
+ expiresIn: result.tokens!.expiresIn,
60
+ },
61
+ 200,
62
+ { "Set-Cookie": cookieHeader }
63
+ );
64
+ } catch (e) {
65
+ return json({ error: "Invalid request body" }, 400);
66
+ }
67
+ }
68
+
69
+ // POST /api/auth/logout - Logout (invalidate refresh token)
70
+ if (path === "/api/auth/logout" && method === "POST") {
71
+ const refreshToken = getRefreshTokenFromCookie(req);
72
+
73
+ if (refreshToken) {
74
+ invalidateSession(refreshToken);
75
+ }
76
+
77
+ // Clear the cookie
78
+ return json(
79
+ { success: true },
80
+ 200,
81
+ { "Set-Cookie": clearRefreshTokenCookie() }
82
+ );
83
+ }
84
+
85
+ // POST /api/auth/refresh - Refresh access token
86
+ if (path === "/api/auth/refresh" && method === "POST") {
87
+ // No users = no valid sessions possible
88
+ if (!UserDB.hasUsers()) {
89
+ return json({ error: "No users exist" }, 401, { "Set-Cookie": clearRefreshTokenCookie() });
90
+ }
91
+
92
+ const refreshToken = getRefreshTokenFromCookie(req);
93
+
94
+ if (!refreshToken) {
95
+ return json({ error: "No refresh token" }, 401);
96
+ }
97
+
98
+ const result = await refreshSession(refreshToken);
99
+
100
+ if (!result) {
101
+ return json(
102
+ { error: "Invalid or expired refresh token" },
103
+ 401,
104
+ { "Set-Cookie": clearRefreshTokenCookie() }
105
+ );
106
+ }
107
+
108
+ // Set new refresh token cookie
109
+ const cookieHeader = createRefreshTokenCookie(result.refreshToken, REFRESH_TOKEN_EXPIRY);
110
+
111
+ return json(
112
+ {
113
+ accessToken: result.accessToken,
114
+ expiresIn: result.expiresIn,
115
+ },
116
+ 200,
117
+ { "Set-Cookie": cookieHeader }
118
+ );
119
+ }
120
+
121
+ // GET /api/auth/me - Get current user
122
+ if (path === "/api/auth/me" && method === "GET") {
123
+ const token = getTokenFromRequest(req);
124
+
125
+ if (!token) {
126
+ return json({ error: "Unauthorized" }, 401);
127
+ }
128
+
129
+ const payload = verifyAccessToken(token);
130
+ if (!payload) {
131
+ return json({ error: "Invalid or expired token" }, 401);
132
+ }
133
+
134
+ const user = UserDB.findById(payload.userId);
135
+ if (!user) {
136
+ return json({ error: "User not found" }, 404);
137
+ }
138
+
139
+ return json({
140
+ user: {
141
+ id: user.id,
142
+ username: user.username,
143
+ email: user.email,
144
+ role: user.role,
145
+ createdAt: user.created_at,
146
+ lastLoginAt: user.last_login_at,
147
+ },
148
+ });
149
+ }
150
+
151
+ // PUT /api/auth/me - Update current user profile
152
+ if (path === "/api/auth/me" && method === "PUT") {
153
+ const token = getTokenFromRequest(req);
154
+
155
+ if (!token) {
156
+ return json({ error: "Unauthorized" }, 401);
157
+ }
158
+
159
+ const payload = verifyAccessToken(token);
160
+ if (!payload) {
161
+ return json({ error: "Invalid or expired token" }, 401);
162
+ }
163
+
164
+ const user = UserDB.findById(payload.userId);
165
+ if (!user) {
166
+ return json({ error: "User not found" }, 404);
167
+ }
168
+
169
+ try {
170
+ const body = await req.json();
171
+ const updates: Parameters<typeof UserDB.update>[1] = {};
172
+
173
+ if (body.email !== undefined) updates.email = body.email;
174
+
175
+ const updated = UserDB.update(user.id, updates);
176
+
177
+ return json({
178
+ user: updated ? {
179
+ id: updated.id,
180
+ username: updated.username,
181
+ email: updated.email,
182
+ role: updated.role,
183
+ createdAt: updated.created_at,
184
+ lastLoginAt: updated.last_login_at,
185
+ } : null,
186
+ });
187
+ } catch (e) {
188
+ return json({ error: "Invalid request body" }, 400);
189
+ }
190
+ }
191
+
192
+ // PUT /api/auth/password - Change password
193
+ if (path === "/api/auth/password" && method === "PUT") {
194
+ const token = getTokenFromRequest(req);
195
+
196
+ if (!token) {
197
+ return json({ error: "Unauthorized" }, 401);
198
+ }
199
+
200
+ const payload = verifyAccessToken(token);
201
+ if (!payload) {
202
+ return json({ error: "Invalid or expired token" }, 401);
203
+ }
204
+
205
+ const user = UserDB.findById(payload.userId);
206
+ if (!user) {
207
+ return json({ error: "User not found" }, 404);
208
+ }
209
+
210
+ try {
211
+ const body = await req.json();
212
+ const { currentPassword, newPassword } = body;
213
+
214
+ if (!currentPassword || !newPassword) {
215
+ return json({ error: "Current and new password are required" }, 400);
216
+ }
217
+
218
+ // Verify current password
219
+ const { verifyPassword } = await import("../auth");
220
+ const isValid = await verifyPassword(currentPassword, user.password_hash);
221
+ if (!isValid) {
222
+ return json({ error: "Current password is incorrect" }, 401);
223
+ }
224
+
225
+ // Validate new password
226
+ const validation = validatePassword(newPassword);
227
+ if (!validation.valid) {
228
+ return json({ error: validation.errors.join(". ") }, 400);
229
+ }
230
+
231
+ // Update password
232
+ const newHash = await hashPassword(newPassword);
233
+ UserDB.update(user.id, { password_hash: newHash });
234
+
235
+ return json({ success: true, message: "Password updated successfully" });
236
+ } catch (e) {
237
+ return json({ error: "Invalid request body" }, 400);
238
+ }
239
+ }
240
+
241
+ return json({ error: "Not found" }, 404);
242
+ }
package/src/server.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import { type Server, type Subprocess } from "bun";
2
2
  import { handleApiRequest } from "./routes/api";
3
+ import { handleAuthRequest } from "./routes/auth";
3
4
  import { serveStatic } from "./routes/static";
4
5
  import { join } from "path";
5
6
  import { homedir } from "os";
6
7
  import { mkdirSync, existsSync } from "fs";
7
8
  import { initDatabase, AgentDB, ProviderKeysDB, McpServerDB, type McpServer, type Agent } from "./db";
9
+ import { authMiddleware, type AuthContext } from "./auth/middleware";
8
10
  import { startMcpProcess } from "./mcp-client";
9
11
  import {
10
12
  ensureBinary,
@@ -112,8 +114,45 @@ const mcpServersToRestart = McpServerDB.findRunning();
112
114
  AgentDB.resetAllStatus();
113
115
  McpServerDB.resetAllStatus();
114
116
 
115
- // In-memory store for running agent processes only
116
- export const agentProcesses: Map<string, Subprocess> = new Map();
117
+ // Clean up orphaned processes on agent ports (targeted cleanup based on DB)
118
+ async function cleanupOrphanedProcesses(): Promise<void> {
119
+ // Get all agents with assigned ports
120
+ const agents = AgentDB.findAll();
121
+ const assignedPorts = agents.map(a => a.port).filter((p): p is number => p !== null);
122
+
123
+ if (assignedPorts.length === 0) return;
124
+
125
+ let cleaned = 0;
126
+ for (const port of assignedPorts) {
127
+ try {
128
+ const res = await fetch(`http://localhost:${port}/health`, { signal: AbortSignal.timeout(200) });
129
+ if (res.ok) {
130
+ // Orphaned process on this port - shut it down
131
+ try {
132
+ await fetch(`http://localhost:${port}/shutdown`, { method: "POST", signal: AbortSignal.timeout(500) });
133
+ cleaned++;
134
+ } catch {
135
+ // Shutdown failed - will be handled when agent tries to start
136
+ }
137
+ }
138
+ } catch {
139
+ // Port not in use - good
140
+ }
141
+ }
142
+
143
+ if (cleaned > 0) {
144
+ console.log(` [cleanup] Stopped ${cleaned} orphaned agent process(es)`);
145
+ }
146
+ }
147
+
148
+ // Run cleanup (don't block startup)
149
+ cleanupOrphanedProcesses().catch(() => {});
150
+
151
+ // In-memory store for running agent processes (agent_id -> { process, port })
152
+ export const agentProcesses: Map<string, { proc: Subprocess; port: number }> = new Map();
153
+
154
+ // Track agents currently being started (to prevent race conditions)
155
+ export const agentsStarting: Set<string> = new Set();
117
156
 
118
157
  // Binary path - can be overridden via environment variable, or found from npm/downloaded
119
158
  export function getBinaryPathForAgent(): string {
@@ -134,9 +173,41 @@ export { getBinaryStatus, BIN_DIR };
134
173
  // Base port for spawned agents
135
174
  export let nextAgentPort = 4100;
136
175
 
137
- // Increment port counter
138
- export function getNextPort(): number {
139
- return nextAgentPort++;
176
+ // Check if a port is available by trying to connect to it
177
+ async function isPortAvailable(port: number): Promise<boolean> {
178
+ try {
179
+ const controller = new AbortController();
180
+ const timeout = setTimeout(() => controller.abort(), 100);
181
+ try {
182
+ await fetch(`http://localhost:${port}/health`, { signal: controller.signal });
183
+ clearTimeout(timeout);
184
+ return false; // Port responded, something is running there
185
+ } catch (err: any) {
186
+ clearTimeout(timeout);
187
+ // Connection refused = port is available
188
+ // Abort error = port is available (timeout means nothing responded)
189
+ if (err?.code === "ECONNREFUSED" || err?.name === "AbortError") {
190
+ return true;
191
+ }
192
+ return true; // Assume available if we get other errors
193
+ }
194
+ } catch {
195
+ return true;
196
+ }
197
+ }
198
+
199
+ // Get next available port (checking that nothing is using it)
200
+ export async function getNextPort(): Promise<number> {
201
+ const maxAttempts = 100; // Prevent infinite loop
202
+ for (let i = 0; i < maxAttempts; i++) {
203
+ const port = nextAgentPort++;
204
+ const available = await isPortAvailable(port);
205
+ if (available) {
206
+ return port;
207
+ }
208
+ console.log(`[port] Port ${port} in use, trying next...`);
209
+ }
210
+ throw new Error("Could not find available port after 100 attempts");
140
211
  }
141
212
 
142
213
  // ANSI color codes matching UI theme
@@ -215,11 +286,23 @@ const server = Bun.serve({
215
286
  const url = new URL(req.url);
216
287
  const path = url.pathname;
217
288
 
218
- // CORS headers
289
+ // Dev mode route logging
290
+ if (process.env.NODE_ENV !== "production" && path.startsWith("/api/")) {
291
+ const params = url.search ? url.search : "";
292
+ console.log(`[${req.method}] ${path}${params}`);
293
+ }
294
+
295
+ // CORS headers - configurable origins
296
+ const origin = req.headers.get("Origin") || "";
297
+ const allowedOrigins = process.env.CORS_ORIGINS?.split(",") || [];
298
+ const isLocalhost = origin.includes("localhost") || origin.includes("127.0.0.1");
299
+ const allowOrigin = allowedOrigins.includes(origin) || isLocalhost || allowedOrigins.length === 0 ? origin || "*" : "";
300
+
219
301
  const corsHeaders = {
220
- "Access-Control-Allow-Origin": "*",
302
+ "Access-Control-Allow-Origin": allowOrigin,
221
303
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
222
- "Access-Control-Allow-Headers": "Content-Type",
304
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
305
+ "Access-Control-Allow-Credentials": "true",
223
306
  };
224
307
 
225
308
  // Handle preflight
@@ -229,8 +312,35 @@ const server = Bun.serve({
229
312
 
230
313
  // API routes
231
314
  if (path.startsWith("/api/")) {
232
- const response = await handleApiRequest(req, path);
233
- // Add CORS headers to response
315
+ // Auth routes handled separately (before middleware)
316
+ if (path.startsWith("/api/auth/")) {
317
+ const response = await handleAuthRequest(req, path);
318
+ Object.entries(corsHeaders).forEach(([key, value]) => {
319
+ response.headers.set(key, value);
320
+ });
321
+ return response;
322
+ }
323
+
324
+ // Health check endpoint (no auth required for Docker health checks)
325
+ if (path === "/api/health") {
326
+ const response = await handleApiRequest(req, path);
327
+ Object.entries(corsHeaders).forEach(([key, value]) => {
328
+ response.headers.set(key, value);
329
+ });
330
+ return response;
331
+ }
332
+
333
+ // Apply auth middleware
334
+ const { response: authResponse, context } = await authMiddleware(req, path);
335
+ if (authResponse) {
336
+ Object.entries(corsHeaders).forEach(([key, value]) => {
337
+ authResponse.headers.set(key, value);
338
+ });
339
+ return authResponse;
340
+ }
341
+
342
+ // Pass auth context to API handler
343
+ const response = await handleApiRequest(req, path, context);
234
344
  Object.entries(corsHeaders).forEach(([key, value]) => {
235
345
  response.headers.set(key, value);
236
346
  });
@@ -290,7 +400,7 @@ if (hasRestarts) {
290
400
  continue;
291
401
  }
292
402
 
293
- const port = getNextPort();
403
+ const port = await getNextPort();
294
404
  const result = await startMcpProcess(server.id, cmd, serverEnv, port);
295
405
 
296
406
  if (result.success) {
package/src/web/App.tsx CHANGED
@@ -7,7 +7,7 @@ import type { Agent, Provider, Route, NewAgentForm } from "./types";
7
7
  import { DEFAULT_FEATURES } from "./types";
8
8
 
9
9
  // Context
10
- import { TelemetryProvider } from "./context";
10
+ import { TelemetryProvider, AuthProvider, ProjectProvider, useAuth, useProjects } from "./context";
11
11
 
12
12
  // Hooks
13
13
  import { useAgents, useProviders, useOnboarding } from "./hooks";
@@ -26,13 +26,26 @@ import {
26
26
  TasksPage,
27
27
  McpPage,
28
28
  TelemetryPage,
29
+ LoginPage,
29
30
  } from "./components";
31
+ import { ApiDocsPage } from "./components/api/ApiDocsPage";
32
+
33
+ function AppContent() {
34
+ // Auth state
35
+ const { isAuthenticated, isLoading: authLoading, hasUsers, accessToken, checkAuth } = useAuth();
36
+ const { refreshProjects } = useProjects();
30
37
 
31
- function App() {
32
38
  // Onboarding state
33
39
  const { isComplete: onboardingComplete, setIsComplete: setOnboardingComplete } = useOnboarding();
34
40
 
35
- // Data hooks
41
+ // Helper to get auth headers
42
+ const getAuthHeaders = (): Record<string, string> => {
43
+ return accessToken ? { Authorization: `Bearer ${accessToken}` } : {};
44
+ };
45
+
46
+ // Data hooks - only fetch when authenticated and onboarding complete
47
+ const shouldFetchData = isAuthenticated && onboardingComplete === true;
48
+
36
49
  const {
37
50
  agents,
38
51
  loading,
@@ -42,13 +55,13 @@ function App() {
42
55
  updateAgent,
43
56
  deleteAgent,
44
57
  toggleAgent,
45
- } = useAgents(onboardingComplete === true);
58
+ } = useAgents(shouldFetchData);
46
59
 
47
60
  const {
48
61
  providers,
49
62
  configuredProviders,
50
63
  fetchProviders,
51
- } = useProviders(onboardingComplete === true);
64
+ } = useProviders(shouldFetchData);
52
65
 
53
66
  // UI state
54
67
  const [showCreate, setShowCreate] = useState(false);
@@ -56,14 +69,15 @@ function App() {
56
69
  const [route, setRoute] = useState<Route>("dashboard");
57
70
  const [startError, setStartError] = useState<string | null>(null);
58
71
  const [taskCount, setTaskCount] = useState(0);
72
+ const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
59
73
 
60
74
  // Fetch task count periodically
61
75
  useEffect(() => {
62
- if (onboardingComplete !== true) return;
76
+ if (!shouldFetchData) return;
63
77
 
64
78
  const fetchTaskCount = async () => {
65
79
  try {
66
- const res = await fetch("/api/tasks?status=pending");
80
+ const res = await fetch("/api/tasks?status=pending", { headers: getAuthHeaders() });
67
81
  if (res.ok) {
68
82
  const data = await res.json();
69
83
  setTaskCount(data.count || 0);
@@ -76,7 +90,7 @@ function App() {
76
90
  fetchTaskCount();
77
91
  const interval = setInterval(fetchTaskCount, 15000);
78
92
  return () => clearInterval(interval);
79
- }, [onboardingComplete]);
93
+ }, [shouldFetchData, accessToken]);
80
94
 
81
95
  // Form state
82
96
  const [newAgent, setNewAgent] = useState<NewAgentForm>({
@@ -125,6 +139,7 @@ function App() {
125
139
  const handleCreateAgent = async () => {
126
140
  if (!newAgent.name) return;
127
141
  await createAgent(newAgent);
142
+ await refreshProjects(); // Refresh project agent counts
128
143
  const defaultProvider = configuredProviders[0];
129
144
  const defaultModel = defaultProvider?.models.find(m => m.recommended)?.value || defaultProvider?.models[0]?.value || "";
130
145
  setNewAgent({
@@ -152,6 +167,7 @@ function App() {
152
167
  setSelectedAgent(null);
153
168
  }
154
169
  await deleteAgent(id);
170
+ await refreshProjects(); // Refresh project agent counts
155
171
  };
156
172
 
157
173
  const handleSelectAgent = (agent: Agent) => {
@@ -168,24 +184,38 @@ function App() {
168
184
  const handleOnboardingComplete = () => {
169
185
  setOnboardingComplete(true);
170
186
  fetchProviders();
187
+ // Refresh auth to pick up new state
188
+ checkAuth();
171
189
  };
172
190
 
191
+ // Show loading while checking auth
192
+ if (authLoading || hasUsers === null) {
193
+ return <LoadingSpinner fullScreen />;
194
+ }
195
+
196
+ // No users exist - show onboarding with account creation
197
+ if (!hasUsers) {
198
+ return <OnboardingWizard onComplete={handleOnboardingComplete} needsAccount={true} />;
199
+ }
200
+
201
+ // Users exist but not authenticated - show login
202
+ if (!isAuthenticated) {
203
+ return <LoginPage />;
204
+ }
205
+
173
206
  // Show loading while checking onboarding
174
207
  if (onboardingComplete === null) {
175
208
  return <LoadingSpinner fullScreen />;
176
209
  }
177
210
 
178
- // Show onboarding if not complete
211
+ // Show onboarding if not complete (but already has account)
179
212
  if (!onboardingComplete) {
180
- return <OnboardingWizard onComplete={handleOnboardingComplete} />;
213
+ return <OnboardingWizard onComplete={handleOnboardingComplete} needsAccount={false} />;
181
214
  }
182
215
 
183
216
  return (
184
217
  <div className="h-screen bg-[#0a0a0a] text-[#e0e0e0] font-mono flex flex-col overflow-hidden">
185
- <Header
186
- onNewAgent={() => setShowCreate(true)}
187
- canCreateAgent={configuredProviders.length > 0}
188
- />
218
+ <Header onMenuClick={() => setMobileMenuOpen(true)} />
189
219
 
190
220
  {startError && (
191
221
  <ErrorBanner message={startError} onDismiss={() => setStartError(null)} />
@@ -197,6 +227,8 @@ function App() {
197
227
  agentCount={agents.length}
198
228
  taskCount={taskCount}
199
229
  onNavigate={handleNavigate}
230
+ isOpen={mobileMenuOpen}
231
+ onClose={() => setMobileMenuOpen(false)}
200
232
  />
201
233
 
202
234
  <main className="flex-1 overflow-hidden flex">
@@ -213,6 +245,8 @@ function App() {
213
245
  onToggleAgent={handleToggleAgent}
214
246
  onDeleteAgent={handleDeleteAgent}
215
247
  onUpdateAgent={updateAgent}
248
+ onNewAgent={() => setShowCreate(true)}
249
+ canCreateAgent={configuredProviders.length > 0}
216
250
  />
217
251
  )}
218
252
 
@@ -232,6 +266,8 @@ function App() {
232
266
  {route === "mcp" && <McpPage />}
233
267
 
234
268
  {route === "telemetry" && <TelemetryPage />}
269
+
270
+ {route === "api" && <ApiDocsPage />}
235
271
  </main>
236
272
  </div>
237
273
 
@@ -254,10 +290,19 @@ function App() {
254
290
  );
255
291
  }
256
292
 
293
+ // Wrapper component that provides all contexts
294
+ function App() {
295
+ return (
296
+ <AuthProvider>
297
+ <ProjectProvider>
298
+ <TelemetryProvider>
299
+ <AppContent />
300
+ </TelemetryProvider>
301
+ </ProjectProvider>
302
+ </AuthProvider>
303
+ );
304
+ }
305
+
257
306
  // Mount the app
258
307
  const root = createRoot(document.getElementById("root")!);
259
- root.render(
260
- <TelemetryProvider>
261
- <App />
262
- </TelemetryProvider>
263
- );
308
+ root.render(<App />);
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
- import { MemoryIcon, TasksIcon, VisionIcon, OperatorIcon, McpIcon, RealtimeIcon } from "../common/Icons";
3
- import { useAgentActivity } from "../../context";
2
+ import { MemoryIcon, TasksIcon, VisionIcon, OperatorIcon, McpIcon, RealtimeIcon, FilesIcon, MultiAgentIcon } from "../common/Icons";
3
+ import { useAgentActivity, useProjects } from "../../context";
4
4
  import type { Agent, AgentFeatures } from "../../types";
5
5
 
6
6
  interface AgentCardProps {
@@ -8,22 +8,26 @@ interface AgentCardProps {
8
8
  selected: boolean;
9
9
  onSelect: () => void;
10
10
  onToggle: (e?: React.MouseEvent) => void;
11
- onDelete: (e?: React.MouseEvent) => void;
11
+ showProject?: boolean;
12
12
  }
13
13
 
14
14
  const FEATURE_ICONS: { key: keyof AgentFeatures; icon: React.ComponentType<{ className?: string }>; label: string }[] = [
15
15
  { key: "memory", icon: MemoryIcon, label: "Memory" },
16
16
  { key: "tasks", icon: TasksIcon, label: "Tasks" },
17
+ { key: "files", icon: FilesIcon, label: "Files" },
17
18
  { key: "vision", icon: VisionIcon, label: "Vision" },
18
19
  { key: "operator", icon: OperatorIcon, label: "Operator" },
19
20
  { key: "mcp", icon: McpIcon, label: "MCP" },
20
21
  { key: "realtime", icon: RealtimeIcon, label: "Realtime" },
22
+ { key: "agents", icon: MultiAgentIcon, label: "Multi-Agent" },
21
23
  ];
22
24
 
23
- export function AgentCard({ agent, selected, onSelect, onToggle, onDelete }: AgentCardProps) {
25
+ export function AgentCard({ agent, selected, onSelect, onToggle, showProject }: AgentCardProps) {
24
26
  const enabledFeatures = FEATURE_ICONS.filter(f => agent.features?.[f.key]);
25
27
  const mcpServers = agent.mcpServerDetails || [];
26
28
  const { isActive, type } = useAgentActivity(agent.id);
29
+ const { projects } = useProjects();
30
+ const project = agent.projectId ? projects.find(p => p.id === agent.projectId) : null;
27
31
 
28
32
  return (
29
33
  <div
@@ -41,6 +45,12 @@ export function AgentCard({ agent, selected, onSelect, onToggle, onDelete }: Age
41
45
  {agent.provider} / {agent.model}
42
46
  {agent.port && <span className="text-[#444]"> · :{agent.port}</span>}
43
47
  </p>
48
+ {showProject && project && (
49
+ <p className="text-sm text-[#666] flex items-center gap-1.5 mt-1">
50
+ <span className="w-2 h-2 rounded-full" style={{ backgroundColor: project.color }} />
51
+ {project.name}
52
+ </p>
53
+ )}
44
54
  </div>
45
55
  <StatusBadge status={agent.status} isActive={isActive && agent.status === "running"} activityType={type} />
46
56
  </div>
@@ -84,24 +94,16 @@ export function AgentCard({ agent, selected, onSelect, onToggle, onDelete }: Age
84
94
  {agent.systemPrompt}
85
95
  </p>
86
96
 
87
- <div className="flex gap-2">
88
- <button
89
- onClick={onToggle}
90
- className={`flex-1 px-3 py-1.5 rounded text-sm font-medium transition ${
91
- agent.status === "running"
92
- ? "bg-[#f97316]/20 text-[#f97316] hover:bg-[#f97316]/30"
93
- : "bg-[#3b82f6]/20 text-[#3b82f6] hover:bg-[#3b82f6]/30"
94
- }`}
95
- >
96
- {agent.status === "running" ? "Stop" : "Start"}
97
- </button>
98
- <button
99
- onClick={onDelete}
100
- className="px-3 py-1.5 rounded text-sm font-medium bg-red-500/20 text-red-400 hover:bg-red-500/30 transition"
101
- >
102
- Delete
103
- </button>
104
- </div>
97
+ <button
98
+ onClick={onToggle}
99
+ className={`w-full px-3 py-1.5 rounded text-sm font-medium transition ${
100
+ agent.status === "running"
101
+ ? "bg-[#f97316]/20 text-[#f97316] hover:bg-[#f97316]/30"
102
+ : "bg-[#3b82f6]/20 text-[#3b82f6] hover:bg-[#3b82f6]/30"
103
+ }`}
104
+ >
105
+ {agent.status === "running" ? "Stop" : "Start"}
106
+ </button>
105
107
  </div>
106
108
  );
107
109
  }