apteva 0.4.4 → 0.4.6

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,269 @@
1
+ import { json, debug } from "./helpers";
2
+ import { startAgentProcess, setAgentStatus } from "./agent-utils";
3
+ import { AgentDB, UserDB } from "../../db";
4
+ import { ProviderKeys, Onboarding, getProvidersWithStatus, PROVIDERS, type ProviderId } from "../../providers";
5
+ import { createUser } from "../../auth";
6
+ import { agentProcesses } from "../../server";
7
+
8
+ export async function handleProviderRoutes(
9
+ req: Request,
10
+ path: string,
11
+ method: string,
12
+ authContext?: unknown,
13
+ ): Promise<Response | null> {
14
+ // ==================== PROVIDERS ====================
15
+
16
+ // GET /api/providers - List supported providers and models with key status
17
+ if (path === "/api/providers" && method === "GET") {
18
+ const providers = getProvidersWithStatus();
19
+ return json({ providers });
20
+ }
21
+
22
+ // GET /api/providers/ollama/models - Fetch available models from Ollama
23
+ if (path === "/api/providers/ollama/models" && method === "GET") {
24
+ // Get configured Ollama base URL or use default
25
+ const ollamaUrl = ProviderKeys.getDecrypted("ollama") || "http://localhost:11434";
26
+
27
+ try {
28
+ const response = await fetch(`${ollamaUrl}/api/tags`, {
29
+ method: "GET",
30
+ headers: { "Accept": "application/json" },
31
+ });
32
+
33
+ if (!response.ok) {
34
+ return json({ error: "Failed to connect to Ollama", models: [] }, 200);
35
+ }
36
+
37
+ const data = await response.json() as { models?: Array<{ name: string; size: number; modified_at: string }> };
38
+ const models = (data.models || []).map((m: { name: string; size: number }) => ({
39
+ value: m.name,
40
+ label: m.name,
41
+ size: m.size,
42
+ }));
43
+
44
+ return json({ models, connected: true });
45
+ } catch (err) {
46
+ // Ollama not running or not reachable
47
+ return json({
48
+ error: "Ollama not reachable. Make sure Ollama is running.",
49
+ models: [],
50
+ connected: false,
51
+ }, 200);
52
+ }
53
+ }
54
+
55
+ // GET /api/providers/ollama/status - Check if Ollama is running
56
+ if (path === "/api/providers/ollama/status" && method === "GET") {
57
+ const ollamaUrl = ProviderKeys.getDecrypted("ollama") || "http://localhost:11434";
58
+
59
+ try {
60
+ const response = await fetch(`${ollamaUrl}/api/tags`, {
61
+ method: "GET",
62
+ signal: AbortSignal.timeout(3000),
63
+ });
64
+
65
+ if (response.ok) {
66
+ const data = await response.json() as { models?: Array<{ name: string }> };
67
+ return json({
68
+ connected: true,
69
+ url: ollamaUrl,
70
+ modelCount: data.models?.length || 0,
71
+ });
72
+ }
73
+ return json({ connected: false, url: ollamaUrl, error: "Ollama not responding" });
74
+ } catch {
75
+ return json({ connected: false, url: ollamaUrl, error: "Ollama not reachable" });
76
+ }
77
+ }
78
+
79
+ // ==================== ONBOARDING ====================
80
+
81
+ // GET /api/onboarding/status - Check onboarding status
82
+ if (path === "/api/onboarding/status" && method === "GET") {
83
+ return json(Onboarding.getStatus());
84
+ }
85
+
86
+ // POST /api/onboarding/complete - Mark onboarding as complete
87
+ if (path === "/api/onboarding/complete" && method === "POST") {
88
+ Onboarding.complete();
89
+ return json({ success: true });
90
+ }
91
+
92
+ // POST /api/onboarding/reset - Reset onboarding (for testing)
93
+ if (path === "/api/onboarding/reset" && method === "POST") {
94
+ Onboarding.reset();
95
+ return json({ success: true });
96
+ }
97
+
98
+ // POST /api/onboarding/user - Create first user during onboarding
99
+ // This endpoint only works when no users exist (enforced by middleware)
100
+ if (path === "/api/onboarding/user" && method === "POST") {
101
+ debug("POST /api/onboarding/user");
102
+ // Double-check no users exist
103
+ if (UserDB.hasUsers()) {
104
+ debug("Users already exist");
105
+ return json({ error: "Users already exist" }, 403);
106
+ }
107
+
108
+ try {
109
+ const body = await req.json();
110
+ debug("Onboarding body:", JSON.stringify(body));
111
+ const { username, password, email } = body;
112
+
113
+ if (!username || !password) {
114
+ debug("Missing username or password");
115
+ return json({ error: "Username and password are required" }, 400);
116
+ }
117
+
118
+ // Create first user as admin
119
+ debug("Creating user:", username);
120
+ const result = await createUser({
121
+ username,
122
+ password,
123
+ email: email || undefined, // Optional, for password recovery
124
+ role: "admin",
125
+ });
126
+ debug("Create user result:", result.success, result.error);
127
+
128
+ if (!result.success) {
129
+ return json({ error: result.error }, 400);
130
+ }
131
+
132
+ return json({
133
+ success: true,
134
+ user: {
135
+ id: result.user!.id,
136
+ username: result.user!.username,
137
+ role: result.user!.role,
138
+ },
139
+ }, 201);
140
+ } catch (e) {
141
+ debug("Onboarding error:", e);
142
+ return json({ error: "Invalid request body" }, 400);
143
+ }
144
+ }
145
+
146
+ // ==================== API KEYS ====================
147
+
148
+ // GET /api/keys - List all configured provider keys (without actual keys)
149
+ if (path === "/api/keys" && method === "GET") {
150
+ return json({ keys: ProviderKeys.getAll() });
151
+ }
152
+
153
+ // GET /api/keys/:provider - Get all keys for a provider
154
+ const getProviderKeysMatch = path.match(/^\/api\/keys\/([^/]+)$/);
155
+
156
+ // POST /api/keys/:provider/test - Test an API key (must match before generic :provider routes)
157
+ const testKeyMatch = path.match(/^\/api\/keys\/([^/]+)\/test$/);
158
+ if (testKeyMatch && method === "POST") {
159
+ const providerId = testKeyMatch[1];
160
+
161
+ // Validate provider exists
162
+ if (!PROVIDERS[providerId as ProviderId]) {
163
+ return json({ error: "Unknown provider" }, 400);
164
+ }
165
+
166
+ try {
167
+ const body = await req.json().catch(() => ({}));
168
+ const { key } = body as { key?: string };
169
+
170
+ // Test with provided key or stored key
171
+ const result = await ProviderKeys.test(providerId, key);
172
+ return json(result);
173
+ } catch (e) {
174
+ return json({ error: "Test failed" }, 500);
175
+ }
176
+ }
177
+
178
+ // DELETE /api/keys/by-id/:id - Remove a specific API key by ID
179
+ const deleteKeyByIdMatch = path.match(/^\/api\/keys\/by-id\/([^/]+)$/);
180
+ if (deleteKeyByIdMatch && method === "DELETE") {
181
+ const keyId = deleteKeyByIdMatch[1];
182
+ const deleted = ProviderKeys.deleteById(keyId);
183
+ return json({ success: deleted });
184
+ }
185
+
186
+ if (getProviderKeysMatch && method === "GET") {
187
+ const providerId = getProviderKeysMatch[1];
188
+ const keys = ProviderKeys.getAllByProvider(providerId);
189
+ return json({ keys });
190
+ }
191
+
192
+ // POST /api/keys/:provider - Save an API key for a provider
193
+ if (getProviderKeysMatch && method === "POST") {
194
+ const providerId = getProviderKeysMatch[1];
195
+
196
+ // Validate provider exists
197
+ if (!PROVIDERS[providerId as ProviderId]) {
198
+ return json({ error: "Unknown provider" }, 400);
199
+ }
200
+
201
+ try {
202
+ const body = await req.json();
203
+ const { key, project_id, name } = body as { key?: string; project_id?: string | null; name?: string | null };
204
+
205
+ if (!key) {
206
+ return json({ error: "API key is required" }, 400);
207
+ }
208
+
209
+ const result = await ProviderKeys.save(providerId, key, project_id || null, name || null);
210
+ if (!result.success) {
211
+ return json({ error: result.error }, 400);
212
+ }
213
+
214
+ // Restart any running agents that use this provider (including meta agent)
215
+ const runningAgents = AgentDB.findAll().filter(
216
+ a => a.status === "running" && a.provider === providerId
217
+ );
218
+
219
+ const restartResults: Array<{ id: string; name: string; success: boolean; error?: string }> = [];
220
+ for (const agent of runningAgents) {
221
+ try {
222
+ // Stop the agent
223
+ const agentProc = agentProcesses.get(agent.id);
224
+ if (agentProc) {
225
+ agentProc.proc.kill();
226
+ agentProcesses.delete(agent.id);
227
+ }
228
+ setAgentStatus(agent.id, "stopped", "provider_restart");
229
+
230
+ // Wait a moment for port to be released
231
+ await new Promise(r => setTimeout(r, 500));
232
+
233
+ // Restart the agent with new key
234
+ const startResult = await startAgentProcess(agent, { silent: true });
235
+ restartResults.push({
236
+ id: agent.id,
237
+ name: agent.name,
238
+ success: startResult.success,
239
+ error: startResult.error,
240
+ });
241
+ } catch (e) {
242
+ restartResults.push({
243
+ id: agent.id,
244
+ name: agent.name,
245
+ success: false,
246
+ error: String(e),
247
+ });
248
+ }
249
+ }
250
+
251
+ return json({
252
+ success: true,
253
+ message: "API key saved successfully",
254
+ restartedAgents: restartResults.length > 0 ? restartResults : undefined,
255
+ });
256
+ } catch (e) {
257
+ return json({ error: "Invalid request body" }, 400);
258
+ }
259
+ }
260
+
261
+ // DELETE /api/keys/:provider - Remove a global API key
262
+ if (getProviderKeysMatch && method === "DELETE") {
263
+ const providerId = getProviderKeysMatch[1];
264
+ const deleted = ProviderKeys.delete(providerId);
265
+ return json({ success: deleted });
266
+ }
267
+
268
+ return null;
269
+ }