apteva 0.1.0
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/LICENSE +63 -0
- package/README.md +84 -0
- package/bin/agent-linux-amd64 +0 -0
- package/bin/apteva.js +144 -0
- package/dist/App.g02zmbqf.js +213 -0
- package/dist/App.g02zmbqf.js.map +37 -0
- package/dist/App.mq6jqare.js +1 -0
- package/dist/apteva-kit.css +1 -0
- package/dist/index.html +14 -0
- package/dist/styles.css +1 -0
- package/package.json +65 -0
- package/src/binary.ts +116 -0
- package/src/crypto.ts +152 -0
- package/src/db.ts +446 -0
- package/src/providers.ts +255 -0
- package/src/routes/api.ts +380 -0
- package/src/routes/static.ts +47 -0
- package/src/server.ts +134 -0
- package/src/web/App.tsx +218 -0
- package/src/web/components/agents/AgentCard.tsx +71 -0
- package/src/web/components/agents/AgentsView.tsx +69 -0
- package/src/web/components/agents/ChatPanel.tsx +63 -0
- package/src/web/components/agents/CreateAgentModal.tsx +128 -0
- package/src/web/components/agents/index.ts +4 -0
- package/src/web/components/common/Icons.tsx +61 -0
- package/src/web/components/common/LoadingSpinner.tsx +44 -0
- package/src/web/components/common/Modal.tsx +16 -0
- package/src/web/components/common/Select.tsx +96 -0
- package/src/web/components/common/index.ts +4 -0
- package/src/web/components/dashboard/Dashboard.tsx +136 -0
- package/src/web/components/dashboard/index.ts +1 -0
- package/src/web/components/index.ts +11 -0
- package/src/web/components/layout/ErrorBanner.tsx +18 -0
- package/src/web/components/layout/Header.tsx +26 -0
- package/src/web/components/layout/Sidebar.tsx +66 -0
- package/src/web/components/layout/index.ts +3 -0
- package/src/web/components/onboarding/OnboardingWizard.tsx +344 -0
- package/src/web/components/onboarding/index.ts +1 -0
- package/src/web/components/settings/SettingsPage.tsx +285 -0
- package/src/web/components/settings/index.ts +1 -0
- package/src/web/hooks/index.ts +3 -0
- package/src/web/hooks/useAgents.ts +62 -0
- package/src/web/hooks/useOnboarding.ts +25 -0
- package/src/web/hooks/useProviders.ts +65 -0
- package/src/web/index.html +21 -0
- package/src/web/styles.css +23 -0
- package/src/web/types.ts +43 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { spawn } from "bun";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { mkdirSync, existsSync } from "fs";
|
|
4
|
+
import { agentProcesses, BINARY_PATH, getNextPort, getBinaryStatus, BIN_DIR } from "../server";
|
|
5
|
+
import { AgentDB, generateId, type Agent } from "../db";
|
|
6
|
+
import { ProviderKeys, Onboarding, getProvidersWithStatus, PROVIDERS, type ProviderId } from "../providers";
|
|
7
|
+
import { binaryExists } from "../binary";
|
|
8
|
+
|
|
9
|
+
// Data directory for agent instances
|
|
10
|
+
const AGENTS_DATA_DIR = join(import.meta.dir, "../../data/agents");
|
|
11
|
+
|
|
12
|
+
function json(data: unknown, status = 200): Response {
|
|
13
|
+
return new Response(JSON.stringify(data), {
|
|
14
|
+
status,
|
|
15
|
+
headers: { "Content-Type": "application/json" },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Transform DB agent to API response format (camelCase for frontend compatibility)
|
|
20
|
+
function toApiAgent(agent: Agent) {
|
|
21
|
+
return {
|
|
22
|
+
id: agent.id,
|
|
23
|
+
name: agent.name,
|
|
24
|
+
model: agent.model,
|
|
25
|
+
provider: agent.provider,
|
|
26
|
+
systemPrompt: agent.system_prompt,
|
|
27
|
+
status: agent.status,
|
|
28
|
+
port: agent.port,
|
|
29
|
+
createdAt: agent.created_at,
|
|
30
|
+
updatedAt: agent.updated_at,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function handleApiRequest(req: Request, path: string): Promise<Response> {
|
|
35
|
+
const method = req.method;
|
|
36
|
+
|
|
37
|
+
// GET /api/agents - List all agents
|
|
38
|
+
if (path === "/api/agents" && method === "GET") {
|
|
39
|
+
const agents = AgentDB.findAll();
|
|
40
|
+
return json({ agents: agents.map(toApiAgent) });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// POST /api/agents - Create a new agent
|
|
44
|
+
if (path === "/api/agents" && method === "POST") {
|
|
45
|
+
try {
|
|
46
|
+
const body = await req.json();
|
|
47
|
+
const { name, model, provider, systemPrompt } = body;
|
|
48
|
+
|
|
49
|
+
if (!name) {
|
|
50
|
+
return json({ error: "Name is required" }, 400);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const agent = AgentDB.create({
|
|
54
|
+
id: generateId(),
|
|
55
|
+
name,
|
|
56
|
+
model: model || "claude-sonnet-4-20250514",
|
|
57
|
+
provider: provider || "anthropic",
|
|
58
|
+
system_prompt: systemPrompt || "You are a helpful assistant.",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return json({ agent: toApiAgent(agent) }, 201);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
console.error("Create agent error:", e);
|
|
64
|
+
return json({ error: "Invalid request body" }, 400);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// GET /api/agents/:id - Get a specific agent
|
|
69
|
+
const agentMatch = path.match(/^\/api\/agents\/([^/]+)$/);
|
|
70
|
+
if (agentMatch && method === "GET") {
|
|
71
|
+
const agent = AgentDB.findById(agentMatch[1]);
|
|
72
|
+
if (!agent) {
|
|
73
|
+
return json({ error: "Agent not found" }, 404);
|
|
74
|
+
}
|
|
75
|
+
return json({ agent: toApiAgent(agent) });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// PUT /api/agents/:id - Update an agent
|
|
79
|
+
if (agentMatch && method === "PUT") {
|
|
80
|
+
const agent = AgentDB.findById(agentMatch[1]);
|
|
81
|
+
if (!agent) {
|
|
82
|
+
return json({ error: "Agent not found" }, 404);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const body = await req.json();
|
|
87
|
+
const updates: Partial<Agent> = {};
|
|
88
|
+
|
|
89
|
+
if (body.name !== undefined) updates.name = body.name;
|
|
90
|
+
if (body.model !== undefined) updates.model = body.model;
|
|
91
|
+
if (body.provider !== undefined) updates.provider = body.provider;
|
|
92
|
+
if (body.systemPrompt !== undefined) updates.system_prompt = body.systemPrompt;
|
|
93
|
+
|
|
94
|
+
const updated = AgentDB.update(agentMatch[1], updates);
|
|
95
|
+
return json({ agent: updated ? toApiAgent(updated) : null });
|
|
96
|
+
} catch (e) {
|
|
97
|
+
return json({ error: "Invalid request body" }, 400);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// DELETE /api/agents/:id - Delete an agent
|
|
102
|
+
if (agentMatch && method === "DELETE") {
|
|
103
|
+
const agent = AgentDB.findById(agentMatch[1]);
|
|
104
|
+
if (!agent) {
|
|
105
|
+
return json({ error: "Agent not found" }, 404);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Stop the agent if running
|
|
109
|
+
const proc = agentProcesses.get(agentMatch[1]);
|
|
110
|
+
if (proc) {
|
|
111
|
+
proc.kill();
|
|
112
|
+
agentProcesses.delete(agentMatch[1]);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
AgentDB.delete(agentMatch[1]);
|
|
116
|
+
return json({ success: true });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// POST /api/agents/:id/start - Start an agent
|
|
120
|
+
const startMatch = path.match(/^\/api\/agents\/([^/]+)\/start$/);
|
|
121
|
+
if (startMatch && method === "POST") {
|
|
122
|
+
const agent = AgentDB.findById(startMatch[1]);
|
|
123
|
+
if (!agent) {
|
|
124
|
+
return json({ error: "Agent not found" }, 404);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check if binary exists
|
|
128
|
+
if (!binaryExists(BIN_DIR)) {
|
|
129
|
+
return json({ error: "Agent binary not available. The binary will be downloaded automatically when available, or you can set AGENT_BINARY_PATH environment variable." }, 400);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Check if already running
|
|
133
|
+
if (agentProcesses.has(agent.id)) {
|
|
134
|
+
return json({ error: "Agent already running" }, 400);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Get the API key for the agent's provider
|
|
138
|
+
const providerKey = ProviderKeys.getDecrypted(agent.provider);
|
|
139
|
+
if (!providerKey) {
|
|
140
|
+
return json({ error: `No API key configured for provider: ${agent.provider}. Please add your API key in Settings.` }, 400);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Get provider config for env var name
|
|
144
|
+
const providerConfig = PROVIDERS[agent.provider as ProviderId];
|
|
145
|
+
if (!providerConfig) {
|
|
146
|
+
return json({ error: `Unknown provider: ${agent.provider}` }, 400);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Assign port
|
|
150
|
+
const port = getNextPort();
|
|
151
|
+
|
|
152
|
+
// Spawn the agent binary
|
|
153
|
+
try {
|
|
154
|
+
// Create data directory for this agent
|
|
155
|
+
const agentDataDir = join(AGENTS_DATA_DIR, agent.id);
|
|
156
|
+
if (!existsSync(agentDataDir)) {
|
|
157
|
+
mkdirSync(agentDataDir, { recursive: true });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log(`Starting agent ${agent.name} on port ${port}...`);
|
|
161
|
+
console.log(` Provider: ${agent.provider}`);
|
|
162
|
+
console.log(` Data dir: ${agentDataDir}`);
|
|
163
|
+
|
|
164
|
+
// Build environment with provider key
|
|
165
|
+
const env: Record<string, string> = {
|
|
166
|
+
...process.env as Record<string, string>,
|
|
167
|
+
PORT: String(port),
|
|
168
|
+
DATA_DIR: agentDataDir,
|
|
169
|
+
[providerConfig.envVar]: providerKey,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const proc = spawn({
|
|
173
|
+
cmd: [BINARY_PATH],
|
|
174
|
+
env,
|
|
175
|
+
stdout: "inherit",
|
|
176
|
+
stderr: "inherit",
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
agentProcesses.set(agent.id, proc);
|
|
180
|
+
|
|
181
|
+
// Update status in database
|
|
182
|
+
const updated = AgentDB.setStatus(agent.id, "running", port);
|
|
183
|
+
|
|
184
|
+
console.log(`Agent ${agent.name} started on port ${port} (pid: ${proc.pid})`);
|
|
185
|
+
return json({ agent: updated ? toApiAgent(updated) : null, message: `Agent started on port ${port}` });
|
|
186
|
+
} catch (err) {
|
|
187
|
+
console.error(`Failed to start agent: ${err}`);
|
|
188
|
+
return json({ error: `Failed to start agent: ${err}` }, 500);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// POST /api/agents/:id/stop - Stop an agent
|
|
193
|
+
const stopMatch = path.match(/^\/api\/agents\/([^/]+)\/stop$/);
|
|
194
|
+
if (stopMatch && method === "POST") {
|
|
195
|
+
const agent = AgentDB.findById(stopMatch[1]);
|
|
196
|
+
if (!agent) {
|
|
197
|
+
return json({ error: "Agent not found" }, 404);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const proc = agentProcesses.get(agent.id);
|
|
201
|
+
if (proc) {
|
|
202
|
+
console.log(`Stopping agent ${agent.name} (pid: ${proc.pid})...`);
|
|
203
|
+
proc.kill();
|
|
204
|
+
agentProcesses.delete(agent.id);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const updated = AgentDB.setStatus(agent.id, "stopped");
|
|
208
|
+
return json({ agent: updated ? toApiAgent(updated) : null, message: "Agent stopped" });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// POST /api/agents/:id/chat - Proxy chat to agent binary with streaming
|
|
212
|
+
const chatMatch = path.match(/^\/api\/agents\/([^/]+)\/chat$/);
|
|
213
|
+
if (chatMatch && method === "POST") {
|
|
214
|
+
const agent = AgentDB.findById(chatMatch[1]);
|
|
215
|
+
if (!agent) {
|
|
216
|
+
return json({ error: "Agent not found" }, 404);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (agent.status !== "running" || !agent.port) {
|
|
220
|
+
return json({ error: "Agent is not running" }, 400);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const body = await req.json();
|
|
225
|
+
|
|
226
|
+
// Proxy to the agent's /chat endpoint
|
|
227
|
+
const agentUrl = `http://localhost:${agent.port}/chat`;
|
|
228
|
+
const response = await fetch(agentUrl, {
|
|
229
|
+
method: "POST",
|
|
230
|
+
headers: {
|
|
231
|
+
"Content-Type": "application/json",
|
|
232
|
+
},
|
|
233
|
+
body: JSON.stringify(body),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Stream the response back
|
|
237
|
+
if (!response.ok) {
|
|
238
|
+
const errorText = await response.text();
|
|
239
|
+
return json({ error: `Agent error: ${errorText}` }, response.status);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Return streaming response with proper headers
|
|
243
|
+
return new Response(response.body, {
|
|
244
|
+
status: 200,
|
|
245
|
+
headers: {
|
|
246
|
+
"Content-Type": response.headers.get("Content-Type") || "text/event-stream",
|
|
247
|
+
"Cache-Control": "no-cache",
|
|
248
|
+
"Connection": "keep-alive",
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
} catch (err) {
|
|
252
|
+
console.error(`Chat proxy error: ${err}`);
|
|
253
|
+
return json({ error: `Failed to proxy chat: ${err}` }, 500);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// GET /api/providers - List supported providers and models with key status
|
|
258
|
+
if (path === "/api/providers" && method === "GET") {
|
|
259
|
+
const providers = getProvidersWithStatus();
|
|
260
|
+
return json({ providers });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ==================== ONBOARDING ====================
|
|
264
|
+
|
|
265
|
+
// GET /api/onboarding/status - Check onboarding status
|
|
266
|
+
if (path === "/api/onboarding/status" && method === "GET") {
|
|
267
|
+
return json(Onboarding.getStatus());
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// POST /api/onboarding/complete - Mark onboarding as complete
|
|
271
|
+
if (path === "/api/onboarding/complete" && method === "POST") {
|
|
272
|
+
Onboarding.complete();
|
|
273
|
+
return json({ success: true });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// POST /api/onboarding/reset - Reset onboarding (for testing)
|
|
277
|
+
if (path === "/api/onboarding/reset" && method === "POST") {
|
|
278
|
+
Onboarding.reset();
|
|
279
|
+
return json({ success: true });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ==================== API KEYS ====================
|
|
283
|
+
|
|
284
|
+
// GET /api/keys - List all configured provider keys (without actual keys)
|
|
285
|
+
if (path === "/api/keys" && method === "GET") {
|
|
286
|
+
return json({ keys: ProviderKeys.getAll() });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// POST /api/keys/:provider - Save an API key for a provider
|
|
290
|
+
const saveKeyMatch = path.match(/^\/api\/keys\/([^/]+)$/);
|
|
291
|
+
if (saveKeyMatch && method === "POST") {
|
|
292
|
+
const providerId = saveKeyMatch[1];
|
|
293
|
+
|
|
294
|
+
// Validate provider exists
|
|
295
|
+
if (!PROVIDERS[providerId as ProviderId]) {
|
|
296
|
+
return json({ error: "Unknown provider" }, 400);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
const body = await req.json();
|
|
301
|
+
const { key } = body;
|
|
302
|
+
|
|
303
|
+
if (!key) {
|
|
304
|
+
return json({ error: "API key is required" }, 400);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const result = await ProviderKeys.save(providerId, key);
|
|
308
|
+
if (!result.success) {
|
|
309
|
+
return json({ error: result.error }, 400);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return json({ success: true, message: "API key saved successfully" });
|
|
313
|
+
} catch (e) {
|
|
314
|
+
return json({ error: "Invalid request body" }, 400);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// DELETE /api/keys/:provider - Remove an API key
|
|
319
|
+
if (saveKeyMatch && method === "DELETE") {
|
|
320
|
+
const providerId = saveKeyMatch[1];
|
|
321
|
+
const deleted = ProviderKeys.delete(providerId);
|
|
322
|
+
return json({ success: deleted });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// POST /api/keys/:provider/test - Test an API key
|
|
326
|
+
const testKeyMatch = path.match(/^\/api\/keys\/([^/]+)\/test$/);
|
|
327
|
+
if (testKeyMatch && method === "POST") {
|
|
328
|
+
const providerId = testKeyMatch[1];
|
|
329
|
+
|
|
330
|
+
// Validate provider exists
|
|
331
|
+
if (!PROVIDERS[providerId as ProviderId]) {
|
|
332
|
+
return json({ error: "Unknown provider" }, 400);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
const body = await req.json().catch(() => ({}));
|
|
337
|
+
const { key } = body as { key?: string };
|
|
338
|
+
|
|
339
|
+
// Test with provided key or stored key
|
|
340
|
+
const result = await ProviderKeys.test(providerId, key);
|
|
341
|
+
return json(result);
|
|
342
|
+
} catch (e) {
|
|
343
|
+
return json({ error: "Test failed" }, 500);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// GET /api/stats - Get statistics
|
|
348
|
+
if (path === "/api/stats" && method === "GET") {
|
|
349
|
+
return json({
|
|
350
|
+
totalAgents: AgentDB.count(),
|
|
351
|
+
runningAgents: AgentDB.countRunning(),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// GET /api/binary - Get binary status
|
|
356
|
+
if (path === "/api/binary" && method === "GET") {
|
|
357
|
+
return json(getBinaryStatus(BIN_DIR));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// GET /api/health - Health check
|
|
361
|
+
if (path === "/api/health") {
|
|
362
|
+
const binaryStatus = getBinaryStatus(BIN_DIR);
|
|
363
|
+
return json({
|
|
364
|
+
status: "ok",
|
|
365
|
+
timestamp: new Date().toISOString(),
|
|
366
|
+
agents: {
|
|
367
|
+
total: AgentDB.count(),
|
|
368
|
+
running: AgentDB.countRunning(),
|
|
369
|
+
},
|
|
370
|
+
binary: {
|
|
371
|
+
available: binaryStatus.exists,
|
|
372
|
+
version: binaryStatus.version,
|
|
373
|
+
platform: binaryStatus.platform,
|
|
374
|
+
arch: binaryStatus.arch,
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return json({ error: "Not found" }, 404);
|
|
380
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
|
|
3
|
+
const DIST_DIR = join(import.meta.dir, "../../dist");
|
|
4
|
+
|
|
5
|
+
export async function serveStatic(req: Request, path: string): Promise<Response> {
|
|
6
|
+
// Default to index.html for root and SPA routes
|
|
7
|
+
let filePath = path === "/" ? "/index.html" : path;
|
|
8
|
+
|
|
9
|
+
// Try to serve the file
|
|
10
|
+
const fullPath = join(DIST_DIR, filePath);
|
|
11
|
+
const file = Bun.file(fullPath);
|
|
12
|
+
|
|
13
|
+
if (await file.exists()) {
|
|
14
|
+
return new Response(file);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// For SPA: if file doesn't exist and it's not a static asset, serve index.html
|
|
18
|
+
if (!path.includes(".")) {
|
|
19
|
+
const indexFile = Bun.file(join(DIST_DIR, "index.html"));
|
|
20
|
+
if (await indexFile.exists()) {
|
|
21
|
+
return new Response(indexFile);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// If dist doesn't exist, serve a development message
|
|
26
|
+
return new Response(
|
|
27
|
+
`<!DOCTYPE html>
|
|
28
|
+
<html>
|
|
29
|
+
<head>
|
|
30
|
+
<title>Apteva</title>
|
|
31
|
+
<style>
|
|
32
|
+
body { font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0f172a; color: #e2e8f0; }
|
|
33
|
+
.container { text-align: center; }
|
|
34
|
+
code { background: #1e293b; padding: 4px 8px; border-radius: 4px; }
|
|
35
|
+
</style>
|
|
36
|
+
</head>
|
|
37
|
+
<body>
|
|
38
|
+
<div class="container">
|
|
39
|
+
<h1>Apteva</h1>
|
|
40
|
+
<p>Run <code>bun run build</code> to build the frontend</p>
|
|
41
|
+
<p>API available at <a href="/api/health" style="color: #60a5fa">/api/health</a></p>
|
|
42
|
+
</div>
|
|
43
|
+
</body>
|
|
44
|
+
</html>`,
|
|
45
|
+
{ headers: { "Content-Type": "text/html" } }
|
|
46
|
+
);
|
|
47
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { type Server, type Subprocess } from "bun";
|
|
2
|
+
import { handleApiRequest } from "./routes/api";
|
|
3
|
+
import { serveStatic } from "./routes/static";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { initDatabase, AgentDB, ProviderKeysDB } from "./db";
|
|
6
|
+
import { ensureBinary, getBinaryPath, getBinaryStatus } from "./binary";
|
|
7
|
+
|
|
8
|
+
const PORT = parseInt(process.env.PORT || "4280");
|
|
9
|
+
const DATA_DIR = process.env.DATA_DIR || join(import.meta.dir, "../data");
|
|
10
|
+
const BIN_DIR = join(import.meta.dir, "../bin");
|
|
11
|
+
|
|
12
|
+
// Load .env file (silently)
|
|
13
|
+
const envPath = join(import.meta.dir, "../.env");
|
|
14
|
+
const envFile = Bun.file(envPath);
|
|
15
|
+
if (await envFile.exists()) {
|
|
16
|
+
const envContent = await envFile.text();
|
|
17
|
+
for (const line of envContent.split("\n")) {
|
|
18
|
+
const [key, ...valueParts] = line.split("=");
|
|
19
|
+
if (key && valueParts.length > 0) {
|
|
20
|
+
process.env[key.trim()] = valueParts.join("=").trim();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Initialize database (silently)
|
|
26
|
+
initDatabase(DATA_DIR);
|
|
27
|
+
|
|
28
|
+
// Reset all agents to stopped on startup (processes don't survive restart)
|
|
29
|
+
AgentDB.resetAllStatus();
|
|
30
|
+
|
|
31
|
+
// In-memory store for running agent processes only
|
|
32
|
+
export const agentProcesses: Map<string, Subprocess> = new Map();
|
|
33
|
+
|
|
34
|
+
// Binary path - can be overridden via environment variable
|
|
35
|
+
export const BINARY_PATH = process.env.AGENT_BINARY_PATH || getBinaryPath(BIN_DIR);
|
|
36
|
+
|
|
37
|
+
// Export binary status function for API
|
|
38
|
+
export { getBinaryStatus, BIN_DIR };
|
|
39
|
+
|
|
40
|
+
// Base port for spawned agents
|
|
41
|
+
export let nextAgentPort = 4100;
|
|
42
|
+
|
|
43
|
+
// Increment port counter
|
|
44
|
+
export function getNextPort(): number {
|
|
45
|
+
return nextAgentPort++;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ANSI color codes matching UI theme
|
|
49
|
+
const c = {
|
|
50
|
+
reset: "\x1b[0m",
|
|
51
|
+
bold: "\x1b[1m",
|
|
52
|
+
dim: "\x1b[2m",
|
|
53
|
+
orange: "\x1b[38;5;208m",
|
|
54
|
+
gray: "\x1b[38;5;245m",
|
|
55
|
+
darkGray: "\x1b[38;5;240m",
|
|
56
|
+
blue: "\x1b[38;5;75m",
|
|
57
|
+
underline: "\x1b[4m",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// OSC 8 hyperlink helper - creates clickable links in supported terminals
|
|
61
|
+
// Works in: iTerm2, Windows Terminal, GNOME Terminal 3.26+, VS Code terminal, Hyper, Kitty
|
|
62
|
+
// Falls back to plain text in unsupported terminals (macOS Terminal.app, older terminals)
|
|
63
|
+
function link(url: string, text?: string): string {
|
|
64
|
+
const displayText = text || url;
|
|
65
|
+
// Using \x1b\\ (ST - String Terminator) instead of \x07 (BEL) for broader compatibility
|
|
66
|
+
return `\x1b]8;;${url}\x1b\\${displayText}\x1b]8;;\x1b\\`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
// Startup banner
|
|
71
|
+
console.log(`
|
|
72
|
+
${c.orange}${c.bold}>_ APTEVA${c.reset}
|
|
73
|
+
${c.gray}Run AI agents locally${c.reset}
|
|
74
|
+
`);
|
|
75
|
+
|
|
76
|
+
// Check binary
|
|
77
|
+
process.stdout.write(` ${c.darkGray}Binary${c.reset} `);
|
|
78
|
+
const binaryResult = await ensureBinary(BIN_DIR);
|
|
79
|
+
if (!binaryResult.success) {
|
|
80
|
+
console.log(`${c.orange}not available${c.reset}`);
|
|
81
|
+
} else {
|
|
82
|
+
console.log(`${c.gray}ready${c.reset}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check database
|
|
86
|
+
process.stdout.write(` ${c.darkGray}Agents${c.reset} `);
|
|
87
|
+
console.log(`${c.gray}${AgentDB.count()} loaded${c.reset}`);
|
|
88
|
+
|
|
89
|
+
// Check providers
|
|
90
|
+
const configuredProviders = ProviderKeysDB.getConfiguredProviders();
|
|
91
|
+
process.stdout.write(` ${c.darkGray}Providers${c.reset} `);
|
|
92
|
+
console.log(`${c.gray}${configuredProviders.length} configured${c.reset}`);
|
|
93
|
+
|
|
94
|
+
const server = Bun.serve({
|
|
95
|
+
port: PORT,
|
|
96
|
+
|
|
97
|
+
async fetch(req: Request): Promise<Response> {
|
|
98
|
+
const url = new URL(req.url);
|
|
99
|
+
const path = url.pathname;
|
|
100
|
+
|
|
101
|
+
// CORS headers
|
|
102
|
+
const corsHeaders = {
|
|
103
|
+
"Access-Control-Allow-Origin": "*",
|
|
104
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
105
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Handle preflight
|
|
109
|
+
if (req.method === "OPTIONS") {
|
|
110
|
+
return new Response(null, { headers: corsHeaders });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// API routes
|
|
114
|
+
if (path.startsWith("/api/")) {
|
|
115
|
+
const response = await handleApiRequest(req, path);
|
|
116
|
+
// Add CORS headers to response
|
|
117
|
+
Object.entries(corsHeaders).forEach(([key, value]) => {
|
|
118
|
+
response.headers.set(key, value);
|
|
119
|
+
});
|
|
120
|
+
return response;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Serve static files (React app)
|
|
124
|
+
return serveStatic(req, path);
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const serverUrl = `http://localhost:${PORT}`;
|
|
129
|
+
console.log(`
|
|
130
|
+
${c.gray}Open${c.reset} ${c.blue}${c.bold}${link(serverUrl)}${c.reset}
|
|
131
|
+
${c.darkGray}Click link or Cmd/Ctrl+C to copy${c.reset}
|
|
132
|
+
`);
|
|
133
|
+
|
|
134
|
+
export default server;
|