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.
- package/dist/App.m4hg4bxq.js +218 -0
- package/dist/index.html +4 -2
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/auth/index.ts +386 -0
- package/src/auth/middleware.ts +183 -0
- package/src/binary.ts +19 -1
- package/src/db.ts +688 -45
- package/src/integrations/composio.ts +437 -0
- package/src/integrations/index.ts +80 -0
- package/src/openapi.ts +1724 -0
- package/src/routes/api.ts +1476 -118
- package/src/routes/auth.ts +242 -0
- package/src/server.ts +121 -11
- package/src/web/App.tsx +64 -19
- package/src/web/components/agents/AgentCard.tsx +24 -22
- package/src/web/components/agents/AgentPanel.tsx +810 -45
- package/src/web/components/agents/AgentsView.tsx +81 -9
- package/src/web/components/agents/CreateAgentModal.tsx +28 -1
- package/src/web/components/api/ApiDocsPage.tsx +583 -0
- package/src/web/components/auth/CreateAccountStep.tsx +176 -0
- package/src/web/components/auth/LoginPage.tsx +91 -0
- package/src/web/components/auth/index.ts +2 -0
- package/src/web/components/common/Icons.tsx +56 -0
- package/src/web/components/common/Modal.tsx +184 -1
- package/src/web/components/dashboard/Dashboard.tsx +70 -22
- package/src/web/components/index.ts +3 -0
- package/src/web/components/layout/Header.tsx +135 -18
- package/src/web/components/layout/Sidebar.tsx +87 -43
- package/src/web/components/mcp/IntegrationsPanel.tsx +743 -0
- package/src/web/components/mcp/McpPage.tsx +451 -63
- package/src/web/components/onboarding/OnboardingWizard.tsx +64 -8
- package/src/web/components/settings/SettingsPage.tsx +340 -26
- package/src/web/components/tasks/TasksPage.tsx +22 -20
- package/src/web/components/telemetry/TelemetryPage.tsx +163 -61
- package/src/web/context/AuthContext.tsx +230 -0
- package/src/web/context/ProjectContext.tsx +182 -0
- package/src/web/context/index.ts +5 -0
- package/src/web/hooks/useAgents.ts +18 -6
- package/src/web/hooks/useOnboarding.ts +20 -4
- package/src/web/hooks/useProviders.ts +15 -5
- package/src/web/icon.png +0 -0
- package/src/web/index.html +1 -1
- package/src/web/styles.css +12 -0
- package/src/web/types.ts +10 -1
- 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
|
-
//
|
|
116
|
-
|
|
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
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
233
|
-
|
|
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
|
-
//
|
|
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(
|
|
58
|
+
} = useAgents(shouldFetchData);
|
|
46
59
|
|
|
47
60
|
const {
|
|
48
61
|
providers,
|
|
49
62
|
configuredProviders,
|
|
50
63
|
fetchProviders,
|
|
51
|
-
} = useProviders(
|
|
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 (
|
|
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
|
-
}, [
|
|
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
|
-
|
|
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,
|
|
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
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
}
|