apteva 0.2.3 → 0.2.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.
- package/dist/App.ggy88vnx.js +213 -0
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +6 -6
- package/src/binary.ts +271 -1
- package/src/crypto.ts +53 -0
- package/src/db.ts +492 -3
- package/src/mcp-client.ts +599 -0
- package/src/providers.ts +31 -0
- package/src/routes/api.ts +786 -63
- package/src/server.ts +122 -5
- package/src/web/App.tsx +36 -1
- package/src/web/components/agents/AgentCard.tsx +22 -1
- package/src/web/components/agents/AgentPanel.tsx +381 -0
- package/src/web/components/agents/AgentsView.tsx +27 -10
- package/src/web/components/agents/CreateAgentModal.tsx +7 -7
- package/src/web/components/agents/index.ts +1 -1
- package/src/web/components/common/Icons.tsx +8 -0
- package/src/web/components/common/Modal.tsx +2 -2
- package/src/web/components/common/Select.tsx +1 -1
- package/src/web/components/common/index.ts +1 -0
- package/src/web/components/dashboard/Dashboard.tsx +74 -25
- package/src/web/components/index.ts +5 -2
- package/src/web/components/layout/Sidebar.tsx +22 -2
- package/src/web/components/mcp/McpPage.tsx +1144 -0
- package/src/web/components/mcp/index.ts +1 -0
- package/src/web/components/onboarding/OnboardingWizard.tsx +5 -1
- package/src/web/components/settings/SettingsPage.tsx +312 -82
- package/src/web/components/tasks/TasksPage.tsx +129 -0
- package/src/web/components/tasks/index.ts +1 -0
- package/src/web/components/telemetry/TelemetryPage.tsx +316 -0
- package/src/web/hooks/useAgents.ts +23 -0
- package/src/web/styles.css +18 -0
- package/src/web/types.ts +75 -1
- package/dist/App.wfhmfhx7.js +0 -213
- package/src/web/components/agents/ChatPanel.tsx +0 -63
package/src/routes/api.ts
CHANGED
|
@@ -3,9 +3,25 @@ import { join } from "path";
|
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import { mkdirSync, existsSync } from "fs";
|
|
5
5
|
import { agentProcesses, BINARY_PATH, getNextPort, getBinaryStatus, BIN_DIR } from "../server";
|
|
6
|
-
import { AgentDB, generateId, type Agent } from "../db";
|
|
6
|
+
import { AgentDB, McpServerDB, TelemetryDB, generateId, type Agent, type AgentFeatures, type McpServer } from "../db";
|
|
7
7
|
import { ProviderKeys, Onboarding, getProvidersWithStatus, PROVIDERS, type ProviderId } from "../providers";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
binaryExists,
|
|
10
|
+
checkForUpdates,
|
|
11
|
+
getInstalledVersion,
|
|
12
|
+
getAptevaVersion,
|
|
13
|
+
downloadLatestBinary,
|
|
14
|
+
installViaNpm,
|
|
15
|
+
} from "../binary";
|
|
16
|
+
import {
|
|
17
|
+
startMcpProcess,
|
|
18
|
+
stopMcpProcess,
|
|
19
|
+
initializeMcpServer,
|
|
20
|
+
listMcpTools,
|
|
21
|
+
callMcpTool,
|
|
22
|
+
getMcpProcess,
|
|
23
|
+
getMcpProxyUrl,
|
|
24
|
+
} from "../mcp-client";
|
|
9
25
|
|
|
10
26
|
// Data directory for agent instances (in ~/.apteva/agents/)
|
|
11
27
|
const AGENTS_DATA_DIR = process.env.DATA_DIR
|
|
@@ -19,8 +35,279 @@ function json(data: unknown, status = 200): Response {
|
|
|
19
35
|
});
|
|
20
36
|
}
|
|
21
37
|
|
|
38
|
+
// Wait for agent to be healthy (with timeout)
|
|
39
|
+
async function waitForAgentHealth(port: number, maxAttempts = 30, delayMs = 200): Promise<boolean> {
|
|
40
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
41
|
+
try {
|
|
42
|
+
const res = await fetch(`http://localhost:${port}/health`, {
|
|
43
|
+
signal: AbortSignal.timeout(1000),
|
|
44
|
+
});
|
|
45
|
+
if (res.ok) return true;
|
|
46
|
+
} catch {
|
|
47
|
+
// Not ready yet
|
|
48
|
+
}
|
|
49
|
+
await new Promise(r => setTimeout(r, delayMs));
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Build agent config from apteva agent data
|
|
55
|
+
// Note: POST /config expects flat structure WITHOUT "agent" wrapper
|
|
56
|
+
function buildAgentConfig(agent: Agent, providerKey: string) {
|
|
57
|
+
const features = agent.features;
|
|
58
|
+
|
|
59
|
+
// Get MCP server details for the agent's selected servers
|
|
60
|
+
// All MCP servers are accessed via HTTP proxy (apteva manages the stdio processes)
|
|
61
|
+
const mcpServers = (agent.mcp_servers || [])
|
|
62
|
+
.map(id => McpServerDB.findById(id))
|
|
63
|
+
.filter((s): s is NonNullable<typeof s> => s !== null && s.status === "running" && s.port)
|
|
64
|
+
.map(s => ({
|
|
65
|
+
name: s.name,
|
|
66
|
+
type: "http" as const,
|
|
67
|
+
url: `http://localhost:${s.port}/mcp`,
|
|
68
|
+
headers: {},
|
|
69
|
+
enabled: true,
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
id: agent.id,
|
|
74
|
+
name: agent.name,
|
|
75
|
+
description: agent.system_prompt,
|
|
76
|
+
llm: {
|
|
77
|
+
provider: agent.provider,
|
|
78
|
+
model: agent.model,
|
|
79
|
+
max_tokens: 4000,
|
|
80
|
+
temperature: 0.7,
|
|
81
|
+
system_prompt: agent.system_prompt,
|
|
82
|
+
vision: {
|
|
83
|
+
enabled: features.vision,
|
|
84
|
+
max_images: 20,
|
|
85
|
+
max_image_size: 5242880,
|
|
86
|
+
allowed_types: ["jpeg", "png", "gif", "webp"],
|
|
87
|
+
resize_images: true,
|
|
88
|
+
max_dimension: 1568,
|
|
89
|
+
pdf: {
|
|
90
|
+
enabled: features.vision,
|
|
91
|
+
max_file_size: 33554432,
|
|
92
|
+
max_pages: 100,
|
|
93
|
+
allow_urls: true,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
parallel_tools: {
|
|
97
|
+
enabled: true,
|
|
98
|
+
max_concurrent: 10,
|
|
99
|
+
},
|
|
100
|
+
tools: [], // Clear any old tool whitelist - agent uses all registered tools
|
|
101
|
+
},
|
|
102
|
+
tasks: {
|
|
103
|
+
enabled: features.tasks,
|
|
104
|
+
allow_scheduling: true,
|
|
105
|
+
allow_recurring: true,
|
|
106
|
+
max_tasks: 100,
|
|
107
|
+
auto_execute: false,
|
|
108
|
+
},
|
|
109
|
+
scheduler: {
|
|
110
|
+
enabled: features.tasks,
|
|
111
|
+
interval: "1m",
|
|
112
|
+
max_tasks: 100,
|
|
113
|
+
},
|
|
114
|
+
memory: {
|
|
115
|
+
enabled: features.memory,
|
|
116
|
+
embedding_model: "text-embedding-3-small",
|
|
117
|
+
decision_model: "gpt-4o-mini",
|
|
118
|
+
max_memories_per_query: 20,
|
|
119
|
+
min_importance: 0.3,
|
|
120
|
+
min_similarity: 0.3,
|
|
121
|
+
auto_prune: true,
|
|
122
|
+
max_memories: 10000,
|
|
123
|
+
embedding_provider: "openai",
|
|
124
|
+
auto_extract_memories: features.memory ? true : null,
|
|
125
|
+
auto_ingest_files: true,
|
|
126
|
+
},
|
|
127
|
+
operator: {
|
|
128
|
+
enabled: features.operator,
|
|
129
|
+
virtual_browser: "http://localhost:8098",
|
|
130
|
+
display_width: 1024,
|
|
131
|
+
display_height: 768,
|
|
132
|
+
max_actions_per_turn: 5,
|
|
133
|
+
},
|
|
134
|
+
mcp: {
|
|
135
|
+
enabled: features.mcp,
|
|
136
|
+
base_url: "http://localhost:3000/mcp",
|
|
137
|
+
timeout: "30s",
|
|
138
|
+
retry_count: 3,
|
|
139
|
+
cache_ttl: "15m",
|
|
140
|
+
servers: mcpServers,
|
|
141
|
+
},
|
|
142
|
+
realtime: {
|
|
143
|
+
enabled: features.realtime,
|
|
144
|
+
provider: "openai",
|
|
145
|
+
model: "gpt-4o-realtime-preview",
|
|
146
|
+
voice: "alloy",
|
|
147
|
+
},
|
|
148
|
+
context: {
|
|
149
|
+
max_messages: 30,
|
|
150
|
+
max_tokens: 0,
|
|
151
|
+
keep_images: 5,
|
|
152
|
+
},
|
|
153
|
+
filesystem: {
|
|
154
|
+
enabled: true,
|
|
155
|
+
max_file_size: 10485760,
|
|
156
|
+
max_total_size: 104857600,
|
|
157
|
+
auto_extract: true,
|
|
158
|
+
auto_cleanup: true,
|
|
159
|
+
retention_days: 7,
|
|
160
|
+
},
|
|
161
|
+
telemetry: {
|
|
162
|
+
enabled: true,
|
|
163
|
+
endpoint: `http://localhost:${process.env.PORT || 4280}/api/telemetry`,
|
|
164
|
+
batch_size: 10,
|
|
165
|
+
flush_interval: 30,
|
|
166
|
+
categories: [], // Empty = all categories
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Push config to running agent
|
|
172
|
+
async function pushConfigToAgent(port: number, config: any): Promise<{ success: boolean; error?: string }> {
|
|
173
|
+
try {
|
|
174
|
+
const res = await fetch(`http://localhost:${port}/config`, {
|
|
175
|
+
method: "POST",
|
|
176
|
+
headers: { "Content-Type": "application/json" },
|
|
177
|
+
body: JSON.stringify(config),
|
|
178
|
+
signal: AbortSignal.timeout(5000),
|
|
179
|
+
});
|
|
180
|
+
if (res.ok) {
|
|
181
|
+
return { success: true };
|
|
182
|
+
}
|
|
183
|
+
const data = await res.json().catch(() => ({}));
|
|
184
|
+
return { success: false, error: data.error || `HTTP ${res.status}` };
|
|
185
|
+
} catch (err) {
|
|
186
|
+
return { success: false, error: String(err) };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Exported helper to start an agent process (used by API route and auto-restart)
|
|
191
|
+
export async function startAgentProcess(
|
|
192
|
+
agent: Agent,
|
|
193
|
+
options: { silent?: boolean } = {}
|
|
194
|
+
): Promise<{ success: boolean; port?: number; error?: string }> {
|
|
195
|
+
const { silent = false } = options;
|
|
196
|
+
|
|
197
|
+
// Check if binary exists
|
|
198
|
+
if (!binaryExists(BIN_DIR)) {
|
|
199
|
+
return { success: false, error: "Agent binary not available" };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Check if already running
|
|
203
|
+
if (agentProcesses.has(agent.id)) {
|
|
204
|
+
return { success: false, error: "Agent already running" };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Get the API key for the agent's provider
|
|
208
|
+
const providerKey = ProviderKeys.getDecrypted(agent.provider);
|
|
209
|
+
if (!providerKey) {
|
|
210
|
+
return { success: false, error: `No API key for provider: ${agent.provider}` };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Get provider config for env var name
|
|
214
|
+
const providerConfig = PROVIDERS[agent.provider as ProviderId];
|
|
215
|
+
if (!providerConfig) {
|
|
216
|
+
return { success: false, error: `Unknown provider: ${agent.provider}` };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Assign port
|
|
220
|
+
const port = getNextPort();
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
// Create data directory for this agent
|
|
224
|
+
const agentDataDir = join(AGENTS_DATA_DIR, agent.id);
|
|
225
|
+
if (!existsSync(agentDataDir)) {
|
|
226
|
+
mkdirSync(agentDataDir, { recursive: true });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!silent) {
|
|
230
|
+
console.log(`Starting agent ${agent.name} on port ${port}...`);
|
|
231
|
+
console.log(` Provider: ${agent.provider}`);
|
|
232
|
+
console.log(` Data dir: ${agentDataDir}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Build environment with provider key
|
|
236
|
+
const env: Record<string, string> = {
|
|
237
|
+
...process.env as Record<string, string>,
|
|
238
|
+
PORT: String(port),
|
|
239
|
+
DATA_DIR: agentDataDir,
|
|
240
|
+
[providerConfig.envVar]: providerKey,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const proc = spawn({
|
|
244
|
+
cmd: [BINARY_PATH],
|
|
245
|
+
env,
|
|
246
|
+
stdout: "ignore",
|
|
247
|
+
stderr: "ignore",
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
agentProcesses.set(agent.id, proc);
|
|
251
|
+
|
|
252
|
+
// Wait for agent to be healthy
|
|
253
|
+
if (!silent) {
|
|
254
|
+
console.log(` Waiting for agent to be ready...`);
|
|
255
|
+
}
|
|
256
|
+
const isHealthy = await waitForAgentHealth(port);
|
|
257
|
+
if (!isHealthy) {
|
|
258
|
+
if (!silent) {
|
|
259
|
+
console.error(` Agent failed to start (health check timeout)`);
|
|
260
|
+
}
|
|
261
|
+
proc.kill();
|
|
262
|
+
agentProcesses.delete(agent.id);
|
|
263
|
+
return { success: false, error: "Health check timeout" };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Push configuration to the agent
|
|
267
|
+
if (!silent) {
|
|
268
|
+
console.log(` Pushing configuration...`);
|
|
269
|
+
}
|
|
270
|
+
const config = buildAgentConfig(agent, providerKey);
|
|
271
|
+
const configResult = await pushConfigToAgent(port, config);
|
|
272
|
+
if (!configResult.success) {
|
|
273
|
+
if (!silent) {
|
|
274
|
+
console.error(` Failed to configure agent: ${configResult.error}`);
|
|
275
|
+
}
|
|
276
|
+
// Agent is running but not configured - still usable but log warning
|
|
277
|
+
} else if (!silent) {
|
|
278
|
+
console.log(` Configuration applied successfully`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Update status in database
|
|
282
|
+
AgentDB.setStatus(agent.id, "running", port);
|
|
283
|
+
|
|
284
|
+
if (!silent) {
|
|
285
|
+
console.log(`Agent ${agent.name} started on port ${port} (pid: ${proc.pid})`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return { success: true, port };
|
|
289
|
+
} catch (err) {
|
|
290
|
+
if (!silent) {
|
|
291
|
+
console.error(`Failed to start agent: ${err}`);
|
|
292
|
+
}
|
|
293
|
+
return { success: false, error: String(err) };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
22
297
|
// Transform DB agent to API response format (camelCase for frontend compatibility)
|
|
23
298
|
function toApiAgent(agent: Agent) {
|
|
299
|
+
// Look up MCP server details
|
|
300
|
+
const mcpServerDetails = (agent.mcp_servers || [])
|
|
301
|
+
.map(id => McpServerDB.findById(id))
|
|
302
|
+
.filter((s): s is NonNullable<typeof s> => s !== null)
|
|
303
|
+
.map(s => ({
|
|
304
|
+
id: s.id,
|
|
305
|
+
name: s.name,
|
|
306
|
+
type: s.type,
|
|
307
|
+
status: s.status,
|
|
308
|
+
port: s.port,
|
|
309
|
+
}));
|
|
310
|
+
|
|
24
311
|
return {
|
|
25
312
|
id: agent.id,
|
|
26
313
|
name: agent.name,
|
|
@@ -30,6 +317,8 @@ function toApiAgent(agent: Agent) {
|
|
|
30
317
|
status: agent.status,
|
|
31
318
|
port: agent.port,
|
|
32
319
|
features: agent.features,
|
|
320
|
+
mcpServers: agent.mcp_servers, // Keep IDs for backwards compatibility
|
|
321
|
+
mcpServerDetails, // Include full details
|
|
33
322
|
createdAt: agent.created_at,
|
|
34
323
|
updatedAt: agent.updated_at,
|
|
35
324
|
};
|
|
@@ -64,6 +353,7 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
|
|
|
64
353
|
provider: provider || "anthropic",
|
|
65
354
|
system_prompt: systemPrompt || "You are a helpful assistant.",
|
|
66
355
|
features: features || DEFAULT_FEATURES,
|
|
356
|
+
mcp_servers: body.mcpServers || [],
|
|
67
357
|
});
|
|
68
358
|
|
|
69
359
|
return json({ agent: toApiAgent(agent) }, 201);
|
|
@@ -98,8 +388,23 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
|
|
|
98
388
|
if (body.model !== undefined) updates.model = body.model;
|
|
99
389
|
if (body.provider !== undefined) updates.provider = body.provider;
|
|
100
390
|
if (body.systemPrompt !== undefined) updates.system_prompt = body.systemPrompt;
|
|
391
|
+
if (body.features !== undefined) updates.features = body.features;
|
|
392
|
+
if (body.mcpServers !== undefined) updates.mcp_servers = body.mcpServers;
|
|
101
393
|
|
|
102
394
|
const updated = AgentDB.update(agentMatch[1], updates);
|
|
395
|
+
|
|
396
|
+
// If agent is running, push the new config
|
|
397
|
+
if (updated && updated.status === "running" && updated.port) {
|
|
398
|
+
const providerKey = ProviderKeys.getDecrypted(updated.provider);
|
|
399
|
+
if (providerKey) {
|
|
400
|
+
const config = buildAgentConfig(updated, providerKey);
|
|
401
|
+
const configResult = await pushConfigToAgent(updated.port, config);
|
|
402
|
+
if (!configResult.success) {
|
|
403
|
+
console.error(`Failed to push config to running agent: ${configResult.error}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
103
408
|
return json({ agent: updated ? toApiAgent(updated) : null });
|
|
104
409
|
} catch (e) {
|
|
105
410
|
return json({ error: "Invalid request body" }, 400);
|
|
@@ -132,69 +437,13 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
|
|
|
132
437
|
return json({ error: "Agent not found" }, 404);
|
|
133
438
|
}
|
|
134
439
|
|
|
135
|
-
|
|
136
|
-
if (!
|
|
137
|
-
return json({ error:
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Check if already running
|
|
141
|
-
if (agentProcesses.has(agent.id)) {
|
|
142
|
-
return json({ error: "Agent already running" }, 400);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Get the API key for the agent's provider
|
|
146
|
-
const providerKey = ProviderKeys.getDecrypted(agent.provider);
|
|
147
|
-
if (!providerKey) {
|
|
148
|
-
return json({ error: `No API key configured for provider: ${agent.provider}. Please add your API key in Settings.` }, 400);
|
|
440
|
+
const result = await startAgentProcess(agent);
|
|
441
|
+
if (!result.success) {
|
|
442
|
+
return json({ error: result.error }, 400);
|
|
149
443
|
}
|
|
150
444
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (!providerConfig) {
|
|
154
|
-
return json({ error: `Unknown provider: ${agent.provider}` }, 400);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Assign port
|
|
158
|
-
const port = getNextPort();
|
|
159
|
-
|
|
160
|
-
// Spawn the agent binary
|
|
161
|
-
try {
|
|
162
|
-
// Create data directory for this agent
|
|
163
|
-
const agentDataDir = join(AGENTS_DATA_DIR, agent.id);
|
|
164
|
-
if (!existsSync(agentDataDir)) {
|
|
165
|
-
mkdirSync(agentDataDir, { recursive: true });
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
console.log(`Starting agent ${agent.name} on port ${port}...`);
|
|
169
|
-
console.log(` Provider: ${agent.provider}`);
|
|
170
|
-
console.log(` Data dir: ${agentDataDir}`);
|
|
171
|
-
|
|
172
|
-
// Build environment with provider key
|
|
173
|
-
const env: Record<string, string> = {
|
|
174
|
-
...process.env as Record<string, string>,
|
|
175
|
-
PORT: String(port),
|
|
176
|
-
DATA_DIR: agentDataDir,
|
|
177
|
-
[providerConfig.envVar]: providerKey,
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
const proc = spawn({
|
|
181
|
-
cmd: [BINARY_PATH],
|
|
182
|
-
env,
|
|
183
|
-
stdout: "ignore",
|
|
184
|
-
stderr: "ignore",
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
agentProcesses.set(agent.id, proc);
|
|
188
|
-
|
|
189
|
-
// Update status in database
|
|
190
|
-
const updated = AgentDB.setStatus(agent.id, "running", port);
|
|
191
|
-
|
|
192
|
-
console.log(`Agent ${agent.name} started on port ${port} (pid: ${proc.pid})`);
|
|
193
|
-
return json({ agent: updated ? toApiAgent(updated) : null, message: `Agent started on port ${port}` });
|
|
194
|
-
} catch (err) {
|
|
195
|
-
console.error(`Failed to start agent: ${err}`);
|
|
196
|
-
return json({ error: `Failed to start agent: ${err}` }, 500);
|
|
197
|
-
}
|
|
445
|
+
const updated = AgentDB.findById(agent.id);
|
|
446
|
+
return json({ agent: updated ? toApiAgent(updated) : null, message: `Agent started on port ${result.port}` });
|
|
198
447
|
}
|
|
199
448
|
|
|
200
449
|
// POST /api/agents/:id/stop - Stop an agent
|
|
@@ -365,9 +614,40 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
|
|
|
365
614
|
return json(getBinaryStatus(BIN_DIR));
|
|
366
615
|
}
|
|
367
616
|
|
|
617
|
+
// GET /api/version - Check agent binary version info
|
|
618
|
+
if (path === "/api/version" && method === "GET") {
|
|
619
|
+
const versionInfo = await checkForUpdates();
|
|
620
|
+
return json(versionInfo);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// POST /api/version/update - Download/install latest agent binary
|
|
624
|
+
if (path === "/api/version/update" && method === "POST") {
|
|
625
|
+
// Check if any agents are running
|
|
626
|
+
const runningAgents = AgentDB.findAll().filter(a => a.status === "running");
|
|
627
|
+
if (runningAgents.length > 0) {
|
|
628
|
+
return json(
|
|
629
|
+
{ success: false, error: "Cannot update while agents are running. Stop all agents first." },
|
|
630
|
+
{ status: 400 }
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Try npm install first, fall back to direct download
|
|
635
|
+
let result = await installViaNpm();
|
|
636
|
+
if (!result.success) {
|
|
637
|
+
// Fall back to direct download
|
|
638
|
+
result = await downloadLatestBinary(BIN_DIR);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (result.success) {
|
|
642
|
+
return json({ success: true, version: result.version });
|
|
643
|
+
}
|
|
644
|
+
return json({ success: false, error: result.error }, { status: 500 });
|
|
645
|
+
}
|
|
646
|
+
|
|
368
647
|
// GET /api/health - Health check
|
|
369
648
|
if (path === "/api/health") {
|
|
370
649
|
const binaryStatus = getBinaryStatus(BIN_DIR);
|
|
650
|
+
const installedVersion = getInstalledVersion();
|
|
371
651
|
return json({
|
|
372
652
|
status: "ok",
|
|
373
653
|
timestamp: new Date().toISOString(),
|
|
@@ -379,7 +659,117 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
|
|
|
379
659
|
available: binaryStatus.exists,
|
|
380
660
|
platform: binaryStatus.platform,
|
|
381
661
|
arch: binaryStatus.arch,
|
|
662
|
+
version: installedVersion,
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// ==================== TASKS ====================
|
|
668
|
+
|
|
669
|
+
// Helper to fetch from a running agent
|
|
670
|
+
async function fetchFromAgent(port: number, endpoint: string): Promise<any> {
|
|
671
|
+
try {
|
|
672
|
+
const response = await fetch(`http://localhost:${port}${endpoint}`, {
|
|
673
|
+
headers: { "Accept": "application/json" },
|
|
674
|
+
});
|
|
675
|
+
if (response.ok) {
|
|
676
|
+
return await response.json();
|
|
677
|
+
}
|
|
678
|
+
return null;
|
|
679
|
+
} catch {
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// GET /api/tasks - Get all tasks from all running agents
|
|
685
|
+
if (path === "/api/tasks" && method === "GET") {
|
|
686
|
+
const url = new URL(req.url);
|
|
687
|
+
const status = url.searchParams.get("status") || "all";
|
|
688
|
+
|
|
689
|
+
const runningAgents = AgentDB.findAll().filter(a => a.status === "running" && a.port);
|
|
690
|
+
const allTasks: any[] = [];
|
|
691
|
+
|
|
692
|
+
for (const agent of runningAgents) {
|
|
693
|
+
const data = await fetchFromAgent(agent.port!, `/tasks?status=${status}`);
|
|
694
|
+
if (data?.tasks) {
|
|
695
|
+
// Add agent info to each task
|
|
696
|
+
for (const task of data.tasks) {
|
|
697
|
+
allTasks.push({
|
|
698
|
+
...task,
|
|
699
|
+
agentId: agent.id,
|
|
700
|
+
agentName: agent.name,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Sort by created_at descending
|
|
707
|
+
allTasks.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
708
|
+
|
|
709
|
+
return json({ tasks: allTasks, count: allTasks.length });
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// GET /api/agents/:id/tasks - Get tasks from a specific agent
|
|
713
|
+
const agentTasksMatch = path.match(/^\/api\/agents\/([^/]+)\/tasks$/);
|
|
714
|
+
if (agentTasksMatch && method === "GET") {
|
|
715
|
+
const agentId = agentTasksMatch[1];
|
|
716
|
+
const agent = AgentDB.findById(agentId);
|
|
717
|
+
|
|
718
|
+
if (!agent) {
|
|
719
|
+
return json({ error: "Agent not found" }, 404);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (agent.status !== "running" || !agent.port) {
|
|
723
|
+
return json({ error: "Agent is not running" }, 400);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const url = new URL(req.url);
|
|
727
|
+
const status = url.searchParams.get("status") || "all";
|
|
728
|
+
|
|
729
|
+
const data = await fetchFromAgent(agent.port, `/tasks?status=${status}`);
|
|
730
|
+
if (!data) {
|
|
731
|
+
return json({ error: "Failed to fetch tasks from agent" }, 500);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return json(data);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// GET /api/dashboard - Get dashboard statistics
|
|
738
|
+
if (path === "/api/dashboard" && method === "GET") {
|
|
739
|
+
const agents = AgentDB.findAll();
|
|
740
|
+
const runningAgents = agents.filter(a => a.status === "running" && a.port);
|
|
741
|
+
|
|
742
|
+
let totalTasks = 0;
|
|
743
|
+
let pendingTasks = 0;
|
|
744
|
+
let completedTasks = 0;
|
|
745
|
+
let runningTasks = 0;
|
|
746
|
+
|
|
747
|
+
for (const agent of runningAgents) {
|
|
748
|
+
const data = await fetchFromAgent(agent.port!, "/tasks?status=all");
|
|
749
|
+
if (data?.tasks) {
|
|
750
|
+
totalTasks += data.tasks.length;
|
|
751
|
+
for (const task of data.tasks) {
|
|
752
|
+
if (task.status === "pending") pendingTasks++;
|
|
753
|
+
else if (task.status === "completed") completedTasks++;
|
|
754
|
+
else if (task.status === "running") runningTasks++;
|
|
755
|
+
}
|
|
382
756
|
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
return json({
|
|
760
|
+
agents: {
|
|
761
|
+
total: agents.length,
|
|
762
|
+
running: runningAgents.length,
|
|
763
|
+
},
|
|
764
|
+
tasks: {
|
|
765
|
+
total: totalTasks,
|
|
766
|
+
pending: pendingTasks,
|
|
767
|
+
running: runningTasks,
|
|
768
|
+
completed: completedTasks,
|
|
769
|
+
},
|
|
770
|
+
providers: {
|
|
771
|
+
configured: ProviderKeys.getConfiguredProviders().length,
|
|
772
|
+
},
|
|
383
773
|
});
|
|
384
774
|
}
|
|
385
775
|
|
|
@@ -418,5 +808,338 @@ export async function handleApiRequest(req: Request, path: string): Promise<Resp
|
|
|
418
808
|
}
|
|
419
809
|
}
|
|
420
810
|
|
|
811
|
+
// ============ MCP Server API ============
|
|
812
|
+
|
|
813
|
+
// GET /api/mcp/servers - List all MCP servers
|
|
814
|
+
if (path === "/api/mcp/servers" && method === "GET") {
|
|
815
|
+
const servers = McpServerDB.findAll();
|
|
816
|
+
return json({ servers });
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// GET /api/mcp/registry - Search MCP registry for available servers
|
|
820
|
+
if (path === "/api/mcp/registry" && method === "GET") {
|
|
821
|
+
const url = new URL(req.url);
|
|
822
|
+
const search = url.searchParams.get("search") || "";
|
|
823
|
+
const limit = url.searchParams.get("limit") || "20";
|
|
824
|
+
|
|
825
|
+
try {
|
|
826
|
+
const registryUrl = `https://registry.modelcontextprotocol.io/v0/servers?search=${encodeURIComponent(search)}&limit=${limit}`;
|
|
827
|
+
const res = await fetch(registryUrl);
|
|
828
|
+
if (!res.ok) {
|
|
829
|
+
return json({ error: "Failed to fetch registry" }, 500);
|
|
830
|
+
}
|
|
831
|
+
const data = await res.json();
|
|
832
|
+
|
|
833
|
+
// Transform to simpler format
|
|
834
|
+
const servers = (data.servers || []).map((item: any) => {
|
|
835
|
+
const s = item.server;
|
|
836
|
+
const pkg = s.packages?.find((p: any) => p.registryType === "npm");
|
|
837
|
+
return {
|
|
838
|
+
name: s.name,
|
|
839
|
+
description: s.description,
|
|
840
|
+
version: s.version,
|
|
841
|
+
repository: s.repository?.url,
|
|
842
|
+
npmPackage: pkg?.identifier,
|
|
843
|
+
transport: pkg?.transport?.type || "stdio",
|
|
844
|
+
envVars: pkg?.environmentVariables || [],
|
|
845
|
+
};
|
|
846
|
+
}).filter((s: any) => s.npmPackage); // Only show npm packages for now
|
|
847
|
+
|
|
848
|
+
return json({ servers });
|
|
849
|
+
} catch (e) {
|
|
850
|
+
return json({ error: "Failed to search registry" }, 500);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// POST /api/mcp/servers - Create/install a new MCP server
|
|
855
|
+
if (path === "/api/mcp/servers" && method === "POST") {
|
|
856
|
+
try {
|
|
857
|
+
const body = await req.json();
|
|
858
|
+
const { name, type, package: pkg, command, args, env } = body;
|
|
859
|
+
|
|
860
|
+
if (!name) {
|
|
861
|
+
return json({ error: "Name is required" }, 400);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const server = McpServerDB.create({
|
|
865
|
+
id: generateId(),
|
|
866
|
+
name,
|
|
867
|
+
type: type || "npm",
|
|
868
|
+
package: pkg || null,
|
|
869
|
+
command: command || null,
|
|
870
|
+
args: args || null,
|
|
871
|
+
env: env || {},
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
return json({ server }, 201);
|
|
875
|
+
} catch (e) {
|
|
876
|
+
console.error("Create MCP server error:", e);
|
|
877
|
+
return json({ error: "Invalid request body" }, 400);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// GET /api/mcp/servers/:id - Get a specific MCP server
|
|
882
|
+
const mcpServerMatch = path.match(/^\/api\/mcp\/servers\/([^/]+)$/);
|
|
883
|
+
if (mcpServerMatch && method === "GET") {
|
|
884
|
+
const server = McpServerDB.findById(mcpServerMatch[1]);
|
|
885
|
+
if (!server) {
|
|
886
|
+
return json({ error: "MCP server not found" }, 404);
|
|
887
|
+
}
|
|
888
|
+
return json({ server });
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// PUT /api/mcp/servers/:id - Update an MCP server
|
|
892
|
+
if (mcpServerMatch && method === "PUT") {
|
|
893
|
+
const server = McpServerDB.findById(mcpServerMatch[1]);
|
|
894
|
+
if (!server) {
|
|
895
|
+
return json({ error: "MCP server not found" }, 404);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
try {
|
|
899
|
+
const body = await req.json();
|
|
900
|
+
const updates: Partial<McpServer> = {};
|
|
901
|
+
|
|
902
|
+
if (body.name !== undefined) updates.name = body.name;
|
|
903
|
+
if (body.type !== undefined) updates.type = body.type;
|
|
904
|
+
if (body.package !== undefined) updates.package = body.package;
|
|
905
|
+
if (body.command !== undefined) updates.command = body.command;
|
|
906
|
+
if (body.args !== undefined) updates.args = body.args;
|
|
907
|
+
if (body.env !== undefined) updates.env = body.env;
|
|
908
|
+
|
|
909
|
+
const updated = McpServerDB.update(mcpServerMatch[1], updates);
|
|
910
|
+
return json({ server: updated });
|
|
911
|
+
} catch (e) {
|
|
912
|
+
return json({ error: "Invalid request body" }, 400);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// DELETE /api/mcp/servers/:id - Delete an MCP server
|
|
917
|
+
if (mcpServerMatch && method === "DELETE") {
|
|
918
|
+
const server = McpServerDB.findById(mcpServerMatch[1]);
|
|
919
|
+
if (!server) {
|
|
920
|
+
return json({ error: "MCP server not found" }, 404);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Stop if running
|
|
924
|
+
if (server.status === "running") {
|
|
925
|
+
// TODO: Stop the server process
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
McpServerDB.delete(mcpServerMatch[1]);
|
|
929
|
+
return json({ success: true });
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// POST /api/mcp/servers/:id/start - Start an MCP server
|
|
933
|
+
const mcpStartMatch = path.match(/^\/api\/mcp\/servers\/([^/]+)\/start$/);
|
|
934
|
+
if (mcpStartMatch && method === "POST") {
|
|
935
|
+
const server = McpServerDB.findById(mcpStartMatch[1]);
|
|
936
|
+
if (!server) {
|
|
937
|
+
return json({ error: "MCP server not found" }, 404);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
if (server.status === "running") {
|
|
941
|
+
return json({ error: "MCP server already running" }, 400);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Determine command to run
|
|
945
|
+
// Helper to substitute $ENV_VAR references with actual values
|
|
946
|
+
const substituteEnvVars = (str: string, env: Record<string, string>): string => {
|
|
947
|
+
return str.replace(/\$([A-Z_][A-Z0-9_]*)/g, (_, varName) => {
|
|
948
|
+
return env[varName] || '';
|
|
949
|
+
});
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
let cmd: string[];
|
|
953
|
+
const serverEnv = server.env || {};
|
|
954
|
+
|
|
955
|
+
if (server.command) {
|
|
956
|
+
// Custom command - substitute env vars in args
|
|
957
|
+
cmd = server.command.split(" ");
|
|
958
|
+
if (server.args) {
|
|
959
|
+
const substitutedArgs = substituteEnvVars(server.args, serverEnv);
|
|
960
|
+
cmd.push(...substitutedArgs.split(" "));
|
|
961
|
+
}
|
|
962
|
+
} else if (server.package) {
|
|
963
|
+
// npm package - use npx
|
|
964
|
+
cmd = ["npx", "-y", server.package];
|
|
965
|
+
if (server.args) {
|
|
966
|
+
const substitutedArgs = substituteEnvVars(server.args, serverEnv);
|
|
967
|
+
cmd.push(...substitutedArgs.split(" "));
|
|
968
|
+
}
|
|
969
|
+
} else {
|
|
970
|
+
return json({ error: "No command or package specified" }, 400);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Get a port for the HTTP proxy
|
|
974
|
+
const port = getNextPort();
|
|
975
|
+
|
|
976
|
+
console.log(`Starting MCP server ${server.name}...`);
|
|
977
|
+
console.log(` Command: ${cmd.join(" ")}`);
|
|
978
|
+
console.log(` HTTP proxy: http://localhost:${port}/mcp`);
|
|
979
|
+
|
|
980
|
+
// Start the MCP process with stdio pipes + HTTP proxy
|
|
981
|
+
const result = await startMcpProcess(server.id, cmd, server.env || {}, port);
|
|
982
|
+
|
|
983
|
+
if (!result.success) {
|
|
984
|
+
console.error(`Failed to start MCP server: ${result.error}`);
|
|
985
|
+
return json({ error: `Failed to start: ${result.error}` }, 500);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Update status with the HTTP proxy port
|
|
989
|
+
const updated = McpServerDB.setStatus(server.id, "running", port);
|
|
990
|
+
|
|
991
|
+
return json({
|
|
992
|
+
server: updated,
|
|
993
|
+
message: "MCP server started",
|
|
994
|
+
proxyUrl: `http://localhost:${port}/mcp`,
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// POST /api/mcp/servers/:id/stop - Stop an MCP server
|
|
999
|
+
const mcpStopMatch = path.match(/^\/api\/mcp\/servers\/([^/]+)\/stop$/);
|
|
1000
|
+
if (mcpStopMatch && method === "POST") {
|
|
1001
|
+
const server = McpServerDB.findById(mcpStopMatch[1]);
|
|
1002
|
+
if (!server) {
|
|
1003
|
+
return json({ error: "MCP server not found" }, 404);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Stop the MCP process
|
|
1007
|
+
stopMcpProcess(server.id);
|
|
1008
|
+
|
|
1009
|
+
const updated = McpServerDB.setStatus(server.id, "stopped");
|
|
1010
|
+
return json({ server: updated, message: "MCP server stopped" });
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// GET /api/mcp/servers/:id/tools - List tools from an MCP server
|
|
1014
|
+
const mcpToolsMatch = path.match(/^\/api\/mcp\/servers\/([^/]+)\/tools$/);
|
|
1015
|
+
if (mcpToolsMatch && method === "GET") {
|
|
1016
|
+
const server = McpServerDB.findById(mcpToolsMatch[1]);
|
|
1017
|
+
if (!server) {
|
|
1018
|
+
return json({ error: "MCP server not found" }, 404);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// Check if process is running
|
|
1022
|
+
const mcpProcess = getMcpProcess(server.id);
|
|
1023
|
+
if (!mcpProcess) {
|
|
1024
|
+
return json({ error: "MCP server is not running" }, 400);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
try {
|
|
1028
|
+
const serverInfo = await initializeMcpServer(server.id);
|
|
1029
|
+
const tools = await listMcpTools(server.id);
|
|
1030
|
+
|
|
1031
|
+
return json({
|
|
1032
|
+
serverInfo,
|
|
1033
|
+
tools,
|
|
1034
|
+
});
|
|
1035
|
+
} catch (err) {
|
|
1036
|
+
console.error(`Failed to list MCP tools: ${err}`);
|
|
1037
|
+
return json({ error: `Failed to communicate with MCP server: ${err}` }, 500);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// POST /api/mcp/servers/:id/tools/:toolName/call - Call a tool on an MCP server
|
|
1042
|
+
const mcpToolCallMatch = path.match(/^\/api\/mcp\/servers\/([^/]+)\/tools\/([^/]+)\/call$/);
|
|
1043
|
+
if (mcpToolCallMatch && method === "POST") {
|
|
1044
|
+
const server = McpServerDB.findById(mcpToolCallMatch[1]);
|
|
1045
|
+
if (!server) {
|
|
1046
|
+
return json({ error: "MCP server not found" }, 404);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// Check if process is running
|
|
1050
|
+
const mcpProcess = getMcpProcess(server.id);
|
|
1051
|
+
if (!mcpProcess) {
|
|
1052
|
+
return json({ error: "MCP server is not running" }, 400);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
const toolName = decodeURIComponent(mcpToolCallMatch[2]);
|
|
1056
|
+
|
|
1057
|
+
try {
|
|
1058
|
+
const body = await req.json();
|
|
1059
|
+
const args = body.arguments || {};
|
|
1060
|
+
|
|
1061
|
+
const result = await callMcpTool(server.id, toolName, args);
|
|
1062
|
+
|
|
1063
|
+
return json({ result });
|
|
1064
|
+
} catch (err) {
|
|
1065
|
+
console.error(`Failed to call MCP tool: ${err}`);
|
|
1066
|
+
return json({ error: `Failed to call tool: ${err}` }, 500);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// ============ Telemetry Endpoints ============
|
|
1071
|
+
|
|
1072
|
+
// POST /api/telemetry - Receive telemetry events from agents
|
|
1073
|
+
if (path === "/api/telemetry" && method === "POST") {
|
|
1074
|
+
try {
|
|
1075
|
+
const body = await req.json() as {
|
|
1076
|
+
agent_id: string;
|
|
1077
|
+
sent_at: string;
|
|
1078
|
+
events: Array<{
|
|
1079
|
+
id: string;
|
|
1080
|
+
timestamp: string;
|
|
1081
|
+
category: string;
|
|
1082
|
+
type: string;
|
|
1083
|
+
level: string;
|
|
1084
|
+
trace_id?: string;
|
|
1085
|
+
span_id?: string;
|
|
1086
|
+
thread_id?: string;
|
|
1087
|
+
data?: Record<string, unknown>;
|
|
1088
|
+
metadata?: Record<string, unknown>;
|
|
1089
|
+
duration_ms?: number;
|
|
1090
|
+
error?: string;
|
|
1091
|
+
}>;
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
if (!body.agent_id || !body.events) {
|
|
1095
|
+
return json({ error: "agent_id and events are required" }, 400);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// Filter out debug events - too noisy
|
|
1099
|
+
const filteredEvents = body.events.filter(e => e.level !== "debug");
|
|
1100
|
+
const inserted = TelemetryDB.insertBatch(body.agent_id, filteredEvents);
|
|
1101
|
+
return json({ received: body.events.length, inserted });
|
|
1102
|
+
} catch (e) {
|
|
1103
|
+
console.error("Telemetry error:", e);
|
|
1104
|
+
return json({ error: "Invalid telemetry payload" }, 400);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// GET /api/telemetry/events - Query telemetry events
|
|
1109
|
+
if (path === "/api/telemetry/events" && method === "GET") {
|
|
1110
|
+
const url = new URL(req.url);
|
|
1111
|
+
const events = TelemetryDB.query({
|
|
1112
|
+
agent_id: url.searchParams.get("agent_id") || undefined,
|
|
1113
|
+
category: url.searchParams.get("category") || undefined,
|
|
1114
|
+
level: url.searchParams.get("level") || undefined,
|
|
1115
|
+
trace_id: url.searchParams.get("trace_id") || undefined,
|
|
1116
|
+
since: url.searchParams.get("since") || undefined,
|
|
1117
|
+
until: url.searchParams.get("until") || undefined,
|
|
1118
|
+
limit: parseInt(url.searchParams.get("limit") || "100"),
|
|
1119
|
+
offset: parseInt(url.searchParams.get("offset") || "0"),
|
|
1120
|
+
});
|
|
1121
|
+
return json({ events });
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// GET /api/telemetry/usage - Get usage statistics
|
|
1125
|
+
if (path === "/api/telemetry/usage" && method === "GET") {
|
|
1126
|
+
const url = new URL(req.url);
|
|
1127
|
+
const usage = TelemetryDB.getUsage({
|
|
1128
|
+
agent_id: url.searchParams.get("agent_id") || undefined,
|
|
1129
|
+
since: url.searchParams.get("since") || undefined,
|
|
1130
|
+
until: url.searchParams.get("until") || undefined,
|
|
1131
|
+
group_by: (url.searchParams.get("group_by") as "agent" | "day") || undefined,
|
|
1132
|
+
});
|
|
1133
|
+
return json({ usage });
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// GET /api/telemetry/stats - Get summary statistics
|
|
1137
|
+
if (path === "/api/telemetry/stats" && method === "GET") {
|
|
1138
|
+
const url = new URL(req.url);
|
|
1139
|
+
const agentId = url.searchParams.get("agent_id") || undefined;
|
|
1140
|
+
const stats = TelemetryDB.getStats(agentId);
|
|
1141
|
+
return json({ stats });
|
|
1142
|
+
}
|
|
1143
|
+
|
|
421
1144
|
return json({ error: "Not found" }, 404);
|
|
422
1145
|
}
|