apteva 0.4.3 → 0.4.5

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.
@@ -0,0 +1,215 @@
1
+ import { json } from "./helpers";
2
+ import { META_AGENT_ENABLED, fetchFromAgent, startAgentProcess, setAgentStatus } from "./agent-utils";
3
+ import { AgentDB } from "../../db";
4
+ import { ProviderKeys } from "../../providers";
5
+ import { agentProcesses, getBinaryStatus, BIN_DIR } from "../../server";
6
+ import {
7
+ checkForUpdates,
8
+ getInstalledVersion,
9
+ getAptevaVersion,
10
+ downloadLatestBinary,
11
+ installViaNpm,
12
+ } from "../../binary";
13
+ import { openApiSpec } from "../../openapi";
14
+
15
+ export async function handleSystemRoutes(
16
+ req: Request,
17
+ path: string,
18
+ method: string,
19
+ authContext?: unknown,
20
+ ): Promise<Response | null> {
21
+ // GET /api/health - Health check endpoint (no auth required)
22
+ if (path === "/api/health" && method === "GET") {
23
+ const binaryStatus = getBinaryStatus(BIN_DIR);
24
+ const installedVersion = getInstalledVersion();
25
+ return json({
26
+ status: "ok",
27
+ version: getAptevaVersion(),
28
+ timestamp: new Date().toISOString(),
29
+ agents: {
30
+ total: AgentDB.count(),
31
+ running: AgentDB.countRunning(),
32
+ },
33
+ binary: {
34
+ available: binaryStatus.exists,
35
+ platform: binaryStatus.platform,
36
+ arch: binaryStatus.arch,
37
+ version: installedVersion,
38
+ },
39
+ });
40
+ }
41
+
42
+ // GET /api/features - Feature flags (no auth required)
43
+ if (path === "/api/features" && method === "GET") {
44
+ return json({
45
+ projects: process.env.PROJECTS_ENABLED === "true",
46
+ metaAgent: process.env.META_AGENT_ENABLED === "true",
47
+ });
48
+ }
49
+
50
+ // GET /api/openapi - OpenAPI spec (no auth required)
51
+ if (path === "/api/openapi" && method === "GET") {
52
+ return json(openApiSpec);
53
+ }
54
+
55
+ // GET /api/stats - Get statistics
56
+ if (path === "/api/stats" && method === "GET") {
57
+ return json({
58
+ totalAgents: AgentDB.count(),
59
+ runningAgents: AgentDB.countRunning(),
60
+ });
61
+ }
62
+
63
+ // GET /api/binary - Get binary status
64
+ if (path === "/api/binary" && method === "GET") {
65
+ return json(getBinaryStatus(BIN_DIR));
66
+ }
67
+
68
+ // GET /api/version - Check agent binary version info
69
+ if (path === "/api/version" && method === "GET") {
70
+ const versionInfo = await checkForUpdates();
71
+ return json(versionInfo);
72
+ }
73
+
74
+ // POST /api/version/update - Download/install latest agent binary
75
+ if (path === "/api/version/update" && method === "POST") {
76
+ // Get all running agents to restart later
77
+ const runningAgents = AgentDB.findAll().filter(a => a.status === "running");
78
+ const agentsToRestart = runningAgents.map(a => a.id);
79
+
80
+ // Stop all running agents
81
+ for (const agent of runningAgents) {
82
+ const agentProc = agentProcesses.get(agent.id);
83
+ if (agentProc) {
84
+ console.log(`Stopping agent ${agent.name} for update...`);
85
+ agentProc.proc.kill();
86
+ agentProcesses.delete(agent.id);
87
+ }
88
+ setAgentStatus(agent.id, "stopped", "binary_update");
89
+ }
90
+
91
+ // Try npm install first, fall back to direct download
92
+ let result = await installViaNpm();
93
+ if (!result.success) {
94
+ // Fall back to direct download
95
+ result = await downloadLatestBinary(BIN_DIR);
96
+ }
97
+
98
+ if (!result.success) {
99
+ return json({ success: false, error: result.error }, 500);
100
+ }
101
+
102
+ // Restart agents that were running
103
+ const restartResults: { id: string; name: string; success: boolean; error?: string }[] = [];
104
+ for (const agentId of agentsToRestart) {
105
+ const agent = AgentDB.findById(agentId);
106
+ if (agent) {
107
+ console.log(`Restarting agent ${agent.name} after update...`);
108
+ const startResult = await startAgentProcess(agent);
109
+ restartResults.push({
110
+ id: agent.id,
111
+ name: agent.name,
112
+ success: startResult.success,
113
+ error: startResult.error,
114
+ });
115
+ }
116
+ }
117
+
118
+ return json({
119
+ success: true,
120
+ version: result.version,
121
+ restarted: restartResults,
122
+ });
123
+ }
124
+
125
+ // GET /api/tasks - Get all tasks from all running agents
126
+ if (path === "/api/tasks" && method === "GET") {
127
+ const url = new URL(req.url);
128
+ const status = url.searchParams.get("status") || "all";
129
+
130
+ const runningAgents = AgentDB.findAll().filter(a => a.status === "running" && a.port);
131
+ const allTasks: any[] = [];
132
+
133
+ for (const agent of runningAgents) {
134
+ const data = await fetchFromAgent(agent.id, agent.port!, `/tasks?status=${status}`);
135
+ if (data?.tasks) {
136
+ // Add agent info to each task
137
+ for (const task of data.tasks) {
138
+ allTasks.push({
139
+ ...task,
140
+ agentId: agent.id,
141
+ agentName: agent.name,
142
+ });
143
+ }
144
+ }
145
+ }
146
+
147
+ // Sort by created_at descending
148
+ allTasks.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
149
+
150
+ return json({ tasks: allTasks, count: allTasks.length });
151
+ }
152
+
153
+ // GET /api/tasks/:agentId/:taskId - Get a single task with full details
154
+ const singleTaskMatch = path.match(/^\/api\/tasks\/([^/]+)\/([^/]+)$/);
155
+ if (singleTaskMatch && method === "GET") {
156
+ const [, agentId, taskId] = singleTaskMatch;
157
+ const agent = AgentDB.findById(agentId);
158
+
159
+ if (!agent) {
160
+ return json({ error: "Agent not found" }, 404);
161
+ }
162
+
163
+ if (agent.status !== "running" || !agent.port) {
164
+ return json({ error: "Agent is not running" }, 400);
165
+ }
166
+
167
+ const data = await fetchFromAgent(agent.id, agent.port, `/tasks/${taskId}`);
168
+ if (!data) {
169
+ return json({ error: "Failed to fetch task from agent" }, 500);
170
+ }
171
+
172
+ return json({ task: { ...data, agentId: agent.id, agentName: agent.name } });
173
+ }
174
+
175
+ // GET /api/dashboard - Get dashboard statistics
176
+ if (path === "/api/dashboard" && method === "GET") {
177
+ const agents = AgentDB.findAll();
178
+ const runningAgents = agents.filter(a => a.status === "running" && a.port);
179
+
180
+ let totalTasks = 0;
181
+ let pendingTasks = 0;
182
+ let completedTasks = 0;
183
+ let runningTasks = 0;
184
+
185
+ for (const agent of runningAgents) {
186
+ const data = await fetchFromAgent(agent.id, agent.port!, "/tasks?status=all");
187
+ if (data?.tasks) {
188
+ totalTasks += data.tasks.length;
189
+ for (const task of data.tasks) {
190
+ if (task.status === "pending") pendingTasks++;
191
+ else if (task.status === "completed") completedTasks++;
192
+ else if (task.status === "running") runningTasks++;
193
+ }
194
+ }
195
+ }
196
+
197
+ return json({
198
+ agents: {
199
+ total: agents.length,
200
+ running: runningAgents.length,
201
+ },
202
+ tasks: {
203
+ total: totalTasks,
204
+ pending: pendingTasks,
205
+ running: runningTasks,
206
+ completed: completedTasks,
207
+ },
208
+ providers: {
209
+ configured: ProviderKeys.getConfiguredProviders().length,
210
+ },
211
+ });
212
+ }
213
+
214
+ return null;
215
+ }
@@ -0,0 +1,142 @@
1
+ import { json } from "./helpers";
2
+ import { TelemetryDB } from "../../db";
3
+ import { telemetryBroadcaster, type TelemetryEvent } from "../../server";
4
+
5
+ export async function handleTelemetryRoutes(
6
+ req: Request,
7
+ path: string,
8
+ method: string,
9
+ ): Promise<Response | null> {
10
+ // POST /api/telemetry - Receive telemetry events from agents
11
+ if (path === "/api/telemetry" && method === "POST") {
12
+ try {
13
+ const body = await req.json() as {
14
+ agent_id: string;
15
+ sent_at: string;
16
+ events: Array<{
17
+ id: string;
18
+ timestamp: string;
19
+ category: string;
20
+ type: string;
21
+ level: string;
22
+ trace_id?: string;
23
+ span_id?: string;
24
+ thread_id?: string;
25
+ data?: Record<string, unknown>;
26
+ metadata?: Record<string, unknown>;
27
+ duration_ms?: number;
28
+ error?: string;
29
+ }>;
30
+ };
31
+
32
+ if (!body.agent_id || !body.events) {
33
+ return json({ error: "agent_id and events are required" }, 400);
34
+ }
35
+
36
+ // Filter out debug events - too noisy
37
+ const filteredEvents = body.events.filter(e => e.level !== "debug");
38
+ const inserted = TelemetryDB.insertBatch(body.agent_id, filteredEvents);
39
+
40
+ // Broadcast to SSE clients
41
+ if (filteredEvents.length > 0) {
42
+ const broadcastEvents: TelemetryEvent[] = filteredEvents.map(e => ({
43
+ id: e.id,
44
+ agent_id: body.agent_id,
45
+ timestamp: e.timestamp,
46
+ category: e.category,
47
+ type: e.type,
48
+ level: e.level,
49
+ trace_id: e.trace_id,
50
+ thread_id: e.thread_id,
51
+ data: e.data,
52
+ duration_ms: e.duration_ms,
53
+ error: e.error,
54
+ }));
55
+ telemetryBroadcaster.broadcast(broadcastEvents);
56
+ }
57
+
58
+ return json({ received: body.events.length, inserted });
59
+ } catch (e) {
60
+ console.error("Telemetry error:", e);
61
+ return json({ error: "Invalid telemetry payload" }, 400);
62
+ }
63
+ }
64
+
65
+ // GET /api/telemetry/stream - SSE stream for real-time telemetry
66
+ if (path === "/api/telemetry/stream" && method === "GET") {
67
+ let controller: ReadableStreamDefaultController<string>;
68
+
69
+ const stream = new ReadableStream<string>({
70
+ start(c) {
71
+ controller = c;
72
+ telemetryBroadcaster.addClient(controller);
73
+ // Send initial connection message
74
+ controller.enqueue("data: {\"connected\":true}\n\n");
75
+ },
76
+ cancel() {
77
+ telemetryBroadcaster.removeClient(controller);
78
+ },
79
+ });
80
+
81
+ return new Response(stream, {
82
+ headers: {
83
+ "Content-Type": "text/event-stream",
84
+ "Cache-Control": "no-cache, no-transform",
85
+ "Connection": "keep-alive",
86
+ "X-Accel-Buffering": "no",
87
+ },
88
+ });
89
+ }
90
+
91
+ // GET /api/telemetry/events - Query telemetry events
92
+ if (path === "/api/telemetry/events" && method === "GET") {
93
+ const url = new URL(req.url);
94
+ const projectIdParam = url.searchParams.get("project_id");
95
+ const events = TelemetryDB.query({
96
+ agent_id: url.searchParams.get("agent_id") || undefined,
97
+ project_id: projectIdParam === "null" ? null : projectIdParam || undefined,
98
+ category: url.searchParams.get("category") || undefined,
99
+ level: url.searchParams.get("level") || undefined,
100
+ trace_id: url.searchParams.get("trace_id") || undefined,
101
+ since: url.searchParams.get("since") || undefined,
102
+ until: url.searchParams.get("until") || undefined,
103
+ limit: parseInt(url.searchParams.get("limit") || "100"),
104
+ offset: parseInt(url.searchParams.get("offset") || "0"),
105
+ });
106
+ return json({ events });
107
+ }
108
+
109
+ // GET /api/telemetry/usage - Get usage statistics
110
+ if (path === "/api/telemetry/usage" && method === "GET") {
111
+ const url = new URL(req.url);
112
+ const projectIdParam = url.searchParams.get("project_id");
113
+ const usage = TelemetryDB.getUsage({
114
+ agent_id: url.searchParams.get("agent_id") || undefined,
115
+ project_id: projectIdParam === "null" ? null : projectIdParam || undefined,
116
+ since: url.searchParams.get("since") || undefined,
117
+ until: url.searchParams.get("until") || undefined,
118
+ group_by: (url.searchParams.get("group_by") as "agent" | "day") || undefined,
119
+ });
120
+ return json({ usage });
121
+ }
122
+
123
+ // GET /api/telemetry/stats - Get summary statistics
124
+ if (path === "/api/telemetry/stats" && method === "GET") {
125
+ const url = new URL(req.url);
126
+ const agentId = url.searchParams.get("agent_id") || undefined;
127
+ const projectIdParam = url.searchParams.get("project_id");
128
+ const stats = TelemetryDB.getStats({
129
+ agentId,
130
+ projectId: projectIdParam === "null" ? null : projectIdParam || undefined,
131
+ });
132
+ return json({ stats });
133
+ }
134
+
135
+ // POST /api/telemetry/clear - Clear all telemetry data
136
+ if (path === "/api/telemetry/clear" && method === "POST") {
137
+ const deleted = TelemetryDB.deleteOlderThan(0); // Delete all
138
+ return json({ deleted });
139
+ }
140
+
141
+ return null;
142
+ }
@@ -0,0 +1,148 @@
1
+ import { json } from "./helpers";
2
+ import { UserDB } from "../../db";
3
+ import { createUser, hashPassword, validatePassword } from "../../auth";
4
+ import type { AuthContext } from "../../auth/middleware";
5
+
6
+ export async function handleUserRoutes(
7
+ req: Request,
8
+ path: string,
9
+ method: string,
10
+ authContext?: AuthContext,
11
+ ): Promise<Response | null> {
12
+ const user = authContext?.user;
13
+
14
+ // GET /api/users - List all users
15
+ if (path === "/api/users" && method === "GET") {
16
+ const users = UserDB.findAll().map(u => ({
17
+ id: u.id,
18
+ username: u.username,
19
+ email: u.email,
20
+ role: u.role,
21
+ createdAt: u.created_at,
22
+ lastLoginAt: u.last_login_at,
23
+ }));
24
+ return json({ users });
25
+ }
26
+
27
+ // POST /api/users - Create a new user
28
+ if (path === "/api/users" && method === "POST") {
29
+ try {
30
+ const body = await req.json();
31
+ const { username, password, email, role } = body;
32
+
33
+ if (!username || !password) {
34
+ return json({ error: "Username and password are required" }, 400);
35
+ }
36
+
37
+ const result = await createUser({
38
+ username,
39
+ password,
40
+ email: email || undefined,
41
+ role: role || "user",
42
+ });
43
+
44
+ if (!result.success) {
45
+ return json({ error: result.error }, 400);
46
+ }
47
+
48
+ return json({
49
+ user: {
50
+ id: result.user!.id,
51
+ username: result.user!.username,
52
+ email: result.user!.email,
53
+ role: result.user!.role,
54
+ createdAt: result.user!.created_at,
55
+ },
56
+ }, 201);
57
+ } catch (e) {
58
+ return json({ error: "Invalid request body" }, 400);
59
+ }
60
+ }
61
+
62
+ // GET /api/users/:id - Get a specific user
63
+ const userMatch = path.match(/^\/api\/users\/([^/]+)$/);
64
+ if (userMatch && method === "GET") {
65
+ const targetUser = UserDB.findById(userMatch[1]);
66
+ if (!targetUser) {
67
+ return json({ error: "User not found" }, 404);
68
+ }
69
+ return json({
70
+ user: {
71
+ id: targetUser.id,
72
+ username: targetUser.username,
73
+ email: targetUser.email,
74
+ role: targetUser.role,
75
+ createdAt: targetUser.created_at,
76
+ lastLoginAt: targetUser.last_login_at,
77
+ },
78
+ });
79
+ }
80
+
81
+ // PUT /api/users/:id - Update a user
82
+ if (userMatch && method === "PUT") {
83
+ const targetUser = UserDB.findById(userMatch[1]);
84
+ if (!targetUser) {
85
+ return json({ error: "User not found" }, 404);
86
+ }
87
+
88
+ try {
89
+ const body = await req.json();
90
+ const updates: Parameters<typeof UserDB.update>[1] = {};
91
+
92
+ if (body.email !== undefined) updates.email = body.email;
93
+ if (body.role !== undefined) {
94
+ // Prevent removing last admin
95
+ if (targetUser.role === "admin" && body.role !== "admin") {
96
+ if (UserDB.countAdmins() <= 1) {
97
+ return json({ error: "Cannot remove the last admin" }, 400);
98
+ }
99
+ }
100
+ updates.role = body.role;
101
+ }
102
+ if (body.password !== undefined) {
103
+ const validation = validatePassword(body.password);
104
+ if (!validation.valid) {
105
+ return json({ error: validation.errors.join(". ") }, 400);
106
+ }
107
+ updates.password_hash = await hashPassword(body.password);
108
+ }
109
+
110
+ const updated = UserDB.update(userMatch[1], updates);
111
+ return json({
112
+ user: updated ? {
113
+ id: updated.id,
114
+ username: updated.username,
115
+ email: updated.email,
116
+ role: updated.role,
117
+ createdAt: updated.created_at,
118
+ lastLoginAt: updated.last_login_at,
119
+ } : null,
120
+ });
121
+ } catch (e) {
122
+ return json({ error: "Invalid request body" }, 400);
123
+ }
124
+ }
125
+
126
+ // DELETE /api/users/:id - Delete a user
127
+ if (userMatch && method === "DELETE") {
128
+ const targetUser = UserDB.findById(userMatch[1]);
129
+ if (!targetUser) {
130
+ return json({ error: "User not found" }, 404);
131
+ }
132
+
133
+ // Prevent deleting yourself
134
+ if (user && targetUser.id === user.id) {
135
+ return json({ error: "Cannot delete your own account" }, 400);
136
+ }
137
+
138
+ // Prevent deleting last admin
139
+ if (targetUser.role === "admin" && UserDB.countAdmins() <= 1) {
140
+ return json({ error: "Cannot delete the last admin" }, 400);
141
+ }
142
+
143
+ UserDB.delete(userMatch[1]);
144
+ return json({ success: true });
145
+ }
146
+
147
+ return null;
148
+ }