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.
Files changed (47) hide show
  1. package/LICENSE +63 -0
  2. package/README.md +84 -0
  3. package/bin/agent-linux-amd64 +0 -0
  4. package/bin/apteva.js +144 -0
  5. package/dist/App.g02zmbqf.js +213 -0
  6. package/dist/App.g02zmbqf.js.map +37 -0
  7. package/dist/App.mq6jqare.js +1 -0
  8. package/dist/apteva-kit.css +1 -0
  9. package/dist/index.html +14 -0
  10. package/dist/styles.css +1 -0
  11. package/package.json +65 -0
  12. package/src/binary.ts +116 -0
  13. package/src/crypto.ts +152 -0
  14. package/src/db.ts +446 -0
  15. package/src/providers.ts +255 -0
  16. package/src/routes/api.ts +380 -0
  17. package/src/routes/static.ts +47 -0
  18. package/src/server.ts +134 -0
  19. package/src/web/App.tsx +218 -0
  20. package/src/web/components/agents/AgentCard.tsx +71 -0
  21. package/src/web/components/agents/AgentsView.tsx +69 -0
  22. package/src/web/components/agents/ChatPanel.tsx +63 -0
  23. package/src/web/components/agents/CreateAgentModal.tsx +128 -0
  24. package/src/web/components/agents/index.ts +4 -0
  25. package/src/web/components/common/Icons.tsx +61 -0
  26. package/src/web/components/common/LoadingSpinner.tsx +44 -0
  27. package/src/web/components/common/Modal.tsx +16 -0
  28. package/src/web/components/common/Select.tsx +96 -0
  29. package/src/web/components/common/index.ts +4 -0
  30. package/src/web/components/dashboard/Dashboard.tsx +136 -0
  31. package/src/web/components/dashboard/index.ts +1 -0
  32. package/src/web/components/index.ts +11 -0
  33. package/src/web/components/layout/ErrorBanner.tsx +18 -0
  34. package/src/web/components/layout/Header.tsx +26 -0
  35. package/src/web/components/layout/Sidebar.tsx +66 -0
  36. package/src/web/components/layout/index.ts +3 -0
  37. package/src/web/components/onboarding/OnboardingWizard.tsx +344 -0
  38. package/src/web/components/onboarding/index.ts +1 -0
  39. package/src/web/components/settings/SettingsPage.tsx +285 -0
  40. package/src/web/components/settings/index.ts +1 -0
  41. package/src/web/hooks/index.ts +3 -0
  42. package/src/web/hooks/useAgents.ts +62 -0
  43. package/src/web/hooks/useOnboarding.ts +25 -0
  44. package/src/web/hooks/useProviders.ts +65 -0
  45. package/src/web/index.html +21 -0
  46. package/src/web/styles.css +23 -0
  47. 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;