apteva 0.4.57 → 0.7.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 (142) hide show
  1. package/README.md +216 -54
  2. package/cli.js +35 -0
  3. package/install.js +92 -0
  4. package/package.json +12 -79
  5. package/LICENSE +0 -63
  6. package/bin/apteva.js +0 -196
  7. package/dist/ActivityPage.kxzzb4yc.js +0 -3
  8. package/dist/ApiDocsPage.zq998hbm.js +0 -4
  9. package/dist/App.55rea8mn.js +0 -61
  10. package/dist/App.5ywb23z4.js +0 -53
  11. package/dist/App.6thds120.js +0 -4
  12. package/dist/App.9tctxzqm.js +0 -8
  13. package/dist/App.a8r8ttaz.js +0 -4
  14. package/dist/App.agsv5bje.js +0 -4
  15. package/dist/App.cepapqmx.js +0 -4
  16. package/dist/App.dp041gb3.js +0 -221
  17. package/dist/App.fds72zb5.js +0 -4
  18. package/dist/App.fg9qj2dq.js +0 -4
  19. package/dist/App.ndfejbm9.js +0 -4
  20. package/dist/App.nxmfmq1h.js +0 -13
  21. package/dist/App.qdfyt8ba.js +0 -4
  22. package/dist/App.x2d0ygt6.js +0 -4
  23. package/dist/App.yt9p4nr3.js +0 -20
  24. package/dist/App.zn4mw16t.js +0 -1
  25. package/dist/ConnectionsPage.8r96ryw7.js +0 -3
  26. package/dist/McpPage.3cwh0gnd.js +0 -3
  27. package/dist/SettingsPage.ykgdh5ev.js +0 -3
  28. package/dist/SkillsPage.4np1s65b.js +0 -3
  29. package/dist/TasksPage.4g08t7p6.js +0 -3
  30. package/dist/TelemetryPage.72w9pwcp.js +0 -3
  31. package/dist/TestsPage.z4fk3r7r.js +0 -3
  32. package/dist/ThreadsPage.63tcajeh.js +0 -3
  33. package/dist/apteva-kit.css +0 -1
  34. package/dist/icon.png +0 -0
  35. package/dist/index.html +0 -16
  36. package/dist/styles.css +0 -1
  37. package/scripts/postinstall.mjs +0 -102
  38. package/src/auth/index.ts +0 -394
  39. package/src/auth/middleware.ts +0 -213
  40. package/src/binary.ts +0 -536
  41. package/src/channels/index.ts +0 -40
  42. package/src/channels/telegram.ts +0 -311
  43. package/src/crypto.ts +0 -301
  44. package/src/db-tests.ts +0 -174
  45. package/src/db.ts +0 -3133
  46. package/src/integrations/agentdojo.ts +0 -559
  47. package/src/integrations/composio.ts +0 -437
  48. package/src/integrations/index.ts +0 -87
  49. package/src/integrations/skillsmp.ts +0 -318
  50. package/src/mcp-client.ts +0 -605
  51. package/src/mcp-handler.ts +0 -394
  52. package/src/mcp-platform.ts +0 -2403
  53. package/src/openapi.ts +0 -2410
  54. package/src/providers.ts +0 -597
  55. package/src/routes/api/agent-utils.ts +0 -890
  56. package/src/routes/api/agents.ts +0 -916
  57. package/src/routes/api/api-keys.ts +0 -95
  58. package/src/routes/api/channels.ts +0 -182
  59. package/src/routes/api/helpers.ts +0 -12
  60. package/src/routes/api/integrations.ts +0 -639
  61. package/src/routes/api/mcp.ts +0 -574
  62. package/src/routes/api/meta-agent.ts +0 -195
  63. package/src/routes/api/projects.ts +0 -112
  64. package/src/routes/api/providers.ts +0 -424
  65. package/src/routes/api/skills.ts +0 -537
  66. package/src/routes/api/system.ts +0 -333
  67. package/src/routes/api/telemetry.ts +0 -203
  68. package/src/routes/api/tests.ts +0 -148
  69. package/src/routes/api/triggers.ts +0 -518
  70. package/src/routes/api/users.ts +0 -148
  71. package/src/routes/api/webhooks.ts +0 -171
  72. package/src/routes/api.ts +0 -53
  73. package/src/routes/auth.ts +0 -251
  74. package/src/routes/share.ts +0 -86
  75. package/src/routes/static.ts +0 -131
  76. package/src/server.ts +0 -642
  77. package/src/test-runner.ts +0 -598
  78. package/src/triggers/agentdojo.ts +0 -253
  79. package/src/triggers/composio.ts +0 -264
  80. package/src/triggers/index.ts +0 -71
  81. package/src/tui/AgentList.tsx +0 -145
  82. package/src/tui/App.tsx +0 -102
  83. package/src/tui/Login.tsx +0 -104
  84. package/src/tui/api.ts +0 -72
  85. package/src/tui/index.tsx +0 -7
  86. package/src/web/App.tsx +0 -455
  87. package/src/web/components/activity/ActivityPage.tsx +0 -314
  88. package/src/web/components/activity/index.ts +0 -1
  89. package/src/web/components/agents/AgentCard.tsx +0 -189
  90. package/src/web/components/agents/AgentPanel.tsx +0 -2244
  91. package/src/web/components/agents/AgentsView.tsx +0 -180
  92. package/src/web/components/agents/CreateAgentModal.tsx +0 -475
  93. package/src/web/components/agents/index.ts +0 -4
  94. package/src/web/components/api/ApiDocsPage.tsx +0 -842
  95. package/src/web/components/auth/CreateAccountStep.tsx +0 -176
  96. package/src/web/components/auth/LoginPage.tsx +0 -91
  97. package/src/web/components/auth/index.ts +0 -2
  98. package/src/web/components/common/Icons.tsx +0 -250
  99. package/src/web/components/common/LoadingSpinner.tsx +0 -44
  100. package/src/web/components/common/Modal.tsx +0 -199
  101. package/src/web/components/common/Select.tsx +0 -97
  102. package/src/web/components/common/index.ts +0 -20
  103. package/src/web/components/connections/ConnectionsPage.tsx +0 -54
  104. package/src/web/components/connections/IntegrationsTab.tsx +0 -170
  105. package/src/web/components/connections/OverviewTab.tsx +0 -137
  106. package/src/web/components/connections/TriggersTab.tsx +0 -1346
  107. package/src/web/components/dashboard/Dashboard.tsx +0 -572
  108. package/src/web/components/dashboard/index.ts +0 -1
  109. package/src/web/components/index.ts +0 -21
  110. package/src/web/components/layout/ErrorBanner.tsx +0 -18
  111. package/src/web/components/layout/Header.tsx +0 -332
  112. package/src/web/components/layout/Sidebar.tsx +0 -231
  113. package/src/web/components/layout/index.ts +0 -3
  114. package/src/web/components/mcp/IntegrationsPanel.tsx +0 -857
  115. package/src/web/components/mcp/McpPage.tsx +0 -2515
  116. package/src/web/components/mcp/index.ts +0 -1
  117. package/src/web/components/meta-agent/MetaAgent.tsx +0 -245
  118. package/src/web/components/onboarding/OnboardingWizard.tsx +0 -404
  119. package/src/web/components/onboarding/index.ts +0 -1
  120. package/src/web/components/settings/SettingsPage.tsx +0 -2776
  121. package/src/web/components/settings/index.ts +0 -1
  122. package/src/web/components/skills/SkillsPage.tsx +0 -1200
  123. package/src/web/components/tasks/TasksPage.tsx +0 -1116
  124. package/src/web/components/tasks/index.ts +0 -1
  125. package/src/web/components/telemetry/TelemetryPage.tsx +0 -1129
  126. package/src/web/components/tests/TestsPage.tsx +0 -594
  127. package/src/web/components/threads/ThreadsPage.tsx +0 -315
  128. package/src/web/context/AuthContext.tsx +0 -242
  129. package/src/web/context/ProjectContext.tsx +0 -214
  130. package/src/web/context/TelemetryContext.tsx +0 -299
  131. package/src/web/context/ThemeContext.tsx +0 -90
  132. package/src/web/context/UIModeContext.tsx +0 -49
  133. package/src/web/context/index.ts +0 -12
  134. package/src/web/hooks/index.ts +0 -3
  135. package/src/web/hooks/useAgents.ts +0 -115
  136. package/src/web/hooks/useOnboarding.ts +0 -20
  137. package/src/web/hooks/useProviders.ts +0 -75
  138. package/src/web/icon.png +0 -0
  139. package/src/web/index.html +0 -16
  140. package/src/web/styles.css +0 -118
  141. package/src/web/themes.ts +0 -162
  142. package/src/web/types.ts +0 -298
package/src/server.ts DELETED
@@ -1,642 +0,0 @@
1
- import { type Server, type Subprocess } from "bun";
2
- import { handleApiRequest } from "./routes/api";
3
- import { handleAuthRequest } from "./routes/auth";
4
- import { handleShareRequest, findAgentByShareToken } from "./routes/share";
5
- import { serveStatic } from "./routes/static";
6
- import { join } from "path";
7
- import { homedir } from "os";
8
- import { mkdirSync, existsSync } from "fs";
9
- import { initDatabase, AgentDB, ApiKeyDB, ProviderKeysDB, McpServerDB, ChannelDB, TelemetryDB, UserDB, type McpServer, type Agent } from "./db";
10
- import { authMiddleware, type AuthContext } from "./auth/middleware";
11
- import { verifyAccessToken } from "./auth";
12
- import { startMcpProcess } from "./mcp-client";
13
- import {
14
- ensureBinary,
15
- getBinaryPath,
16
- getBinaryStatus,
17
- getActualBinaryPath,
18
- initVersionTracking,
19
- checkForUpdates,
20
- getInstalledVersion,
21
- getAptevaVersion,
22
- downloadLatestBinary,
23
- } from "./binary";
24
-
25
- // ============ SSE Telemetry Broadcast ============
26
- export interface TelemetryEvent {
27
- id: string;
28
- agent_id: string;
29
- timestamp: string;
30
- category: string;
31
- type: string;
32
- level: string;
33
- trace_id?: string;
34
- thread_id?: string;
35
- data?: Record<string, unknown>;
36
- duration_ms?: number;
37
- error?: string;
38
- cost?: number;
39
- }
40
-
41
- class TelemetryBroadcaster {
42
- private clients: Set<ReadableStreamDefaultController<string>> = new Set();
43
-
44
- addClient(controller: ReadableStreamDefaultController<string>) {
45
- this.clients.add(controller);
46
- console.log(`[SSE] Client connected (${this.clients.size} total)`);
47
- }
48
-
49
- removeClient(controller: ReadableStreamDefaultController<string>) {
50
- this.clients.delete(controller);
51
- console.log(`[SSE] Client disconnected (${this.clients.size} remaining)`);
52
- }
53
-
54
- broadcast(events: TelemetryEvent[]) {
55
- if (this.clients.size === 0) return;
56
-
57
- const data = `data: ${JSON.stringify(events)}\n\n`;
58
- const failedClients: ReadableStreamDefaultController<string>[] = [];
59
-
60
- // Iterate over a copy to avoid modification during iteration
61
- for (const controller of Array.from(this.clients)) {
62
- try {
63
- controller.enqueue(data);
64
- } catch {
65
- failedClients.push(controller);
66
- }
67
- }
68
-
69
- // Remove failed clients after iteration
70
- for (const client of failedClients) {
71
- this.clients.delete(client);
72
- console.log(`[SSE] Removed failed client (${this.clients.size} remaining)`);
73
- }
74
- }
75
-
76
- get clientCount() {
77
- return this.clients.size;
78
- }
79
- }
80
-
81
- export const telemetryBroadcaster = new TelemetryBroadcaster();
82
-
83
- const PORT = parseInt(process.env.PORT || "4280");
84
-
85
- // Use ~/.apteva for persistent data (survives npm updates)
86
- const HOME_DATA_DIR = join(homedir(), ".apteva");
87
- if (!existsSync(HOME_DATA_DIR)) {
88
- mkdirSync(HOME_DATA_DIR, { recursive: true });
89
- }
90
- const DATA_DIR = process.env.DATA_DIR || HOME_DATA_DIR;
91
- const BIN_DIR = join(import.meta.dir, "../bin");
92
-
93
- // Load .env file (silently)
94
- const envPath = join(import.meta.dir, "../.env");
95
- const envFile = Bun.file(envPath);
96
- if (await envFile.exists()) {
97
- const envContent = await envFile.text();
98
- for (const line of envContent.split("\n")) {
99
- const [key, ...valueParts] = line.split("=");
100
- if (key && valueParts.length > 0) {
101
- process.env[key.trim()] = valueParts.join("=").trim();
102
- }
103
- }
104
- }
105
-
106
- // Initialize database (silently)
107
- initDatabase(DATA_DIR);
108
-
109
- // Clean up old telemetry events (keep last 30 days)
110
- try {
111
- const deleted = TelemetryDB.deleteOlderThan(30);
112
- if (deleted > 0) console.log(`[db] Cleaned up ${deleted} telemetry events older than 30 days`);
113
- } catch { /* ignore */ }
114
-
115
- // Initialize version tracking
116
- initVersionTracking(DATA_DIR);
117
-
118
- // Get agents, MCP servers, and channels that were running before restart (for auto-restart)
119
- const agentsToRestart = AgentDB.findRunning();
120
- const mcpServersToRestart = McpServerDB.findRunning();
121
- const channelsToRestart = ChannelDB.findRunning();
122
-
123
- // Reset all agents and MCP servers to stopped on startup (processes don't survive restart)
124
- AgentDB.resetAllStatus();
125
- McpServerDB.resetAllStatus();
126
- // Reset channels too (bot polling doesn't survive restart)
127
- for (const ch of channelsToRestart) {
128
- ChannelDB.setStatus(ch.id, "stopped");
129
- }
130
-
131
- // Clean up orphaned processes on agent ports (targeted cleanup based on DB)
132
- async function cleanupOrphanedProcesses(): Promise<void> {
133
- // Get all agents with assigned ports
134
- const agents = AgentDB.findAll();
135
- const assignedPorts = agents.map(a => a.port).filter((p): p is number => p !== null);
136
-
137
- if (assignedPorts.length === 0) return;
138
-
139
- // Check all ports in parallel
140
- const results = await Promise.allSettled(assignedPorts.map(async (port) => {
141
- try {
142
- const res = await fetch(`http://localhost:${port}/health`, { signal: AbortSignal.timeout(200) });
143
- if (!res.ok) return false;
144
- // Orphaned process - shut it down gracefully
145
- try {
146
- await fetch(`http://localhost:${port}/shutdown`, { method: "POST", signal: AbortSignal.timeout(500) });
147
- await new Promise(r => setTimeout(r, 500));
148
- } catch {}
149
- // Force kill if still running
150
- try {
151
- const check = await fetch(`http://localhost:${port}/health`, { signal: AbortSignal.timeout(200) });
152
- if (check.ok) {
153
- const { execSync } = await import("child_process");
154
- execSync(`lsof -ti :${port} | xargs -r kill -9 2>/dev/null || true`, { stdio: "ignore" });
155
- }
156
- } catch {}
157
- return true;
158
- } catch {
159
- return false;
160
- }
161
- }));
162
-
163
- const cleaned = results.filter(r => r.status === "fulfilled" && r.value).length;
164
- if (cleaned > 0) {
165
- console.log(` [cleanup] Stopped ${cleaned} orphaned agent process(es)`);
166
- }
167
- }
168
-
169
- // Run cleanup (must complete before auto-restart to avoid killing freshly started agents)
170
- await cleanupOrphanedProcesses().catch(() => {});
171
-
172
- // In-memory store for running agent processes (agent_id -> { process, port })
173
- export const agentProcesses: Map<string, { proc: Subprocess; port: number }> = new Map();
174
-
175
- // Track agents currently being started (to prevent race conditions)
176
- export const agentsStarting: Set<string> = new Set();
177
-
178
- // Graceful shutdown handler - stop all agent processes when server exits
179
- // NOTE: We intentionally do NOT update DB status here — agents marked "running"
180
- // in the DB will be auto-restarted on next boot via findRunning() + resetAllStatus().
181
- async function shutdownAllAgents() {
182
- if (agentProcesses.size === 0) return;
183
-
184
- console.log(`\n Stopping ${agentProcesses.size} running agent(s)...`);
185
-
186
- for (const [agentId, { proc, port }] of agentProcesses) {
187
- try {
188
- // Try graceful shutdown
189
- await fetch(`http://localhost:${port}/shutdown`, {
190
- method: "POST",
191
- signal: AbortSignal.timeout(1000),
192
- }).catch(() => {});
193
-
194
- proc.kill();
195
- } catch {
196
- // Ignore errors during shutdown
197
- }
198
- }
199
- agentProcesses.clear();
200
- }
201
-
202
- // Handle process termination signals
203
- let shuttingDown = false;
204
- export function isShuttingDown(): boolean { return shuttingDown; }
205
- async function shutdownAllChannels() {
206
- try {
207
- const { stopAllChannels } = await import("./channels");
208
- await stopAllChannels();
209
- } catch {
210
- // Ignore import/stop errors during shutdown
211
- }
212
- }
213
-
214
- process.on("SIGINT", async () => {
215
- if (shuttingDown) return;
216
- shuttingDown = true;
217
- await shutdownAllChannels();
218
- await shutdownAllAgents();
219
- process.exit(0);
220
- });
221
- process.on("SIGTERM", async () => {
222
- if (shuttingDown) return;
223
- shuttingDown = true;
224
- await shutdownAllChannels();
225
- await shutdownAllAgents();
226
- process.exit(0);
227
- });
228
-
229
- // Binary path - can be overridden via environment variable, or found from npm/downloaded
230
- export function getBinaryPathForAgent(): string {
231
- // Environment override takes priority
232
- if (process.env.AGENT_BINARY_PATH) {
233
- return process.env.AGENT_BINARY_PATH;
234
- }
235
- // Otherwise use downloaded or npm binary (getActualBinaryPath checks both)
236
- const actualPath = getActualBinaryPath(BIN_DIR);
237
- if (actualPath) {
238
- return actualPath;
239
- }
240
- // No binary found - return expected path for error messages
241
- return getBinaryPath(BIN_DIR);
242
- }
243
-
244
- // Export for legacy compatibility
245
- export const BINARY_PATH = getBinaryPathForAgent();
246
-
247
- // Export binary status function for API
248
- export { getBinaryStatus, BIN_DIR };
249
-
250
- // Base port for MCP server proxies (separate range from agents which use 4100-4199)
251
- export let nextMcpPort = 4200;
252
-
253
- // Check if a port is available by trying to connect to it
254
- async function isPortAvailable(port: number): Promise<boolean> {
255
- try {
256
- const controller = new AbortController();
257
- const timeout = setTimeout(() => controller.abort(), 100);
258
- try {
259
- await fetch(`http://localhost:${port}/health`, { signal: controller.signal });
260
- clearTimeout(timeout);
261
- return false; // Port responded, something is running there
262
- } catch (err: any) {
263
- clearTimeout(timeout);
264
- // Connection refused = port is available
265
- // Abort error = port is available (timeout means nothing responded)
266
- if (err?.code === "ECONNREFUSED" || err?.name === "AbortError") {
267
- return true;
268
- }
269
- return true; // Assume available if we get other errors
270
- }
271
- } catch {
272
- return true;
273
- }
274
- }
275
-
276
- // Get next available port for MCP servers (checking that nothing is using it)
277
- export async function getNextPort(): Promise<number> {
278
- const maxAttempts = 100; // Prevent infinite loop
279
- for (let i = 0; i < maxAttempts; i++) {
280
- const port = nextMcpPort++;
281
- const available = await isPortAvailable(port);
282
- if (available) {
283
- return port;
284
- }
285
- console.log(`[port] Port ${port} in use, trying next...`);
286
- }
287
- throw new Error("Could not find available port after 100 attempts");
288
- }
289
-
290
- // ANSI color codes matching UI theme
291
- const c = {
292
- reset: "\x1b[0m",
293
- bold: "\x1b[1m",
294
- dim: "\x1b[2m",
295
- orange: "\x1b[38;5;208m",
296
- gray: "\x1b[38;5;245m",
297
- darkGray: "\x1b[38;5;240m",
298
- blue: "\x1b[38;5;75m",
299
- underline: "\x1b[4m",
300
- };
301
-
302
- // OSC 8 hyperlink helper - creates clickable links in supported terminals
303
- // Works in: iTerm2, Windows Terminal, GNOME Terminal 3.26+, VS Code terminal, Hyper, Kitty
304
- // Falls back to plain text in unsupported terminals (macOS Terminal.app, older terminals)
305
- function link(url: string, text?: string): string {
306
- const displayText = text || url;
307
- // Using \x1b\\ (ST - String Terminator) instead of \x07 (BEL) for broader compatibility
308
- return `\x1b]8;;${url}\x1b\\${displayText}\x1b]8;;\x1b\\`;
309
- }
310
-
311
-
312
- // Startup banner
313
- const aptevaVersion = getAptevaVersion();
314
- console.log(`
315
- ${c.orange}${c.bold}>_ apteva${c.reset} ${c.gray}v${aptevaVersion}${c.reset}
316
- ${c.gray}Run AI agents locally${c.reset}
317
- `);
318
-
319
- // Check binary - ensureBinary handles progress output when downloading
320
- process.stdout.write(` ${c.darkGray}Agent${c.reset} `);
321
- const binaryResult = await ensureBinary(BIN_DIR);
322
- // ensureBinary prints its own status when downloading/failing
323
- // We only need to print "ready" if binary already existed
324
- if (binaryResult.success && !binaryResult.downloaded) {
325
- const installedVersion = getInstalledVersion();
326
- console.log(`${c.gray}v${installedVersion || "unknown"} ready${c.reset}`);
327
- }
328
-
329
- // Check for updates in background (don't block startup)
330
- checkForUpdates().then(versions => {
331
- const updates: string[] = [];
332
- if (versions.apteva.updateAvailable) {
333
- updates.push(`apteva: v${versions.apteva.installed} → v${versions.apteva.latest}`);
334
- }
335
- if (versions.agent.updateAvailable) {
336
- updates.push(`agent: v${versions.agent.installed || "?"} → v${versions.agent.latest}`);
337
- }
338
- if (updates.length > 0) {
339
- console.log(`\n ${c.orange}Updates available:${c.reset}`);
340
- updates.forEach(u => console.log(` ${c.gray}• ${u}${c.reset}`));
341
- console.log(` ${c.gray}Update from Settings or run: npx apteva@latest${c.reset}\n`);
342
- }
343
- }).catch(() => {
344
- // Silently ignore version check failures
345
- });
346
-
347
- // Check database
348
- process.stdout.write(` ${c.darkGray}Agents${c.reset} `);
349
- console.log(`${c.gray}${AgentDB.count()} loaded${c.reset}`);
350
-
351
- // Check providers
352
- const configuredProviders = ProviderKeysDB.getConfiguredProviders();
353
- process.stdout.write(` ${c.darkGray}Providers${c.reset} `);
354
- console.log(`${c.gray}${configuredProviders.length} configured${c.reset}`);
355
-
356
- const server = Bun.serve({
357
- port: PORT,
358
- hostname: "0.0.0.0", // Listen on all interfaces
359
- development: false, // Suppress "Started server" message
360
- idleTimeout: 255, // Max value - prevents SSE connections from timing out
361
-
362
- async fetch(req: Request, bunServer: Server): Promise<Response | undefined> {
363
- const url = new URL(req.url);
364
- const path = url.pathname;
365
-
366
- // Dev mode route logging
367
- if (process.env.NODE_ENV !== "production" && path.startsWith("/api/")) {
368
- const params = url.search ? url.search : "";
369
- console.log(`[${req.method}] ${path}${params}`);
370
- }
371
-
372
- // WebSocket upgrade: /api/agents/:id/voice → proxy to agent binary
373
- const voiceMatch = path.match(/^\/api\/agents\/([^/]+)\/voice$/);
374
- if (voiceMatch && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
375
- // Validate auth via query param token (WebSocket can't send custom headers)
376
- if (process.env.AUTH_ENABLED !== "false") {
377
- const token = url.searchParams.get("token");
378
- if (!token) {
379
- return new Response("Authentication required", { status: 401 });
380
- }
381
- // Try API key first, then JWT
382
- const isApiKey = token.startsWith("apt_");
383
- if (isApiKey) {
384
- const result = ApiKeyDB.validate(token);
385
- if (!result) {
386
- return new Response("Invalid API key", { status: 401 });
387
- }
388
- } else {
389
- const payload = verifyAccessToken(token);
390
- if (!payload) {
391
- return new Response("Invalid or expired token", { status: 401 });
392
- }
393
- const user = UserDB.findById(payload.userId);
394
- if (!user) {
395
- return new Response("User not found", { status: 401 });
396
- }
397
- }
398
- }
399
-
400
- const agentId = voiceMatch[1];
401
- const agent = AgentDB.findById(agentId);
402
- if (!agent || agent.status !== "running" || !agent.port) {
403
- return new Response("Agent not available", { status: 400 });
404
- }
405
- const agentApiKey = AgentDB.getApiKey(agentId) || "";
406
- console.log(`[WS] Voice upgrade: agent=${agentId} port=${agent.port} hasKey=${!!agentApiKey}`);
407
- const upgraded = bunServer.upgrade(req, { data: { agentId: agent.id, agentPort: agent.port, agentApiKey } });
408
- if (upgraded) return undefined;
409
- return new Response("WebSocket upgrade failed", { status: 500 });
410
- }
411
-
412
- // WebSocket upgrade: /share/<token>/voice → proxy to agent binary (public, share-token auth)
413
- const shareVoiceMatch = path.match(/^\/share\/([a-f0-9]{32})\/voice$/);
414
- if (shareVoiceMatch && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
415
- const shareToken = shareVoiceMatch[1];
416
- const agent = findAgentByShareToken(shareToken);
417
- if (!agent || agent.status !== "running" || !agent.port) {
418
- return new Response("Agent not available", { status: 400 });
419
- }
420
- const agentApiKey = AgentDB.getApiKey(agent.id) || "";
421
- console.log(`[WS] Share voice upgrade: agent=${agent.id} port=${agent.port}`);
422
- const upgraded = bunServer.upgrade(req, { data: { agentId: agent.id, agentPort: agent.port, agentApiKey } });
423
- if (upgraded) return undefined;
424
- return new Response("WebSocket upgrade failed", { status: 500 });
425
- }
426
-
427
- // CORS headers - configurable origins
428
- const origin = req.headers.get("Origin") || "";
429
- const allowedOrigins = process.env.CORS_ORIGINS?.split(",") || [];
430
- const isLocalhost = origin.includes("localhost") || origin.includes("127.0.0.1");
431
- const allowOrigin = allowedOrigins.includes(origin) || isLocalhost || allowedOrigins.length === 0 ? origin || "*" : "";
432
-
433
- const corsHeaders = {
434
- "Access-Control-Allow-Origin": allowOrigin,
435
- "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
436
- "Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key",
437
- "Access-Control-Allow-Credentials": "true",
438
- };
439
-
440
- // Handle preflight
441
- if (req.method === "OPTIONS") {
442
- return new Response(null, { headers: corsHeaders });
443
- }
444
-
445
- // API routes
446
- if (path.startsWith("/api/")) {
447
- // Auth routes handled separately (before middleware)
448
- if (path.startsWith("/api/auth/")) {
449
- const response = await handleAuthRequest(req, path);
450
- Object.entries(corsHeaders).forEach(([key, value]) => {
451
- response.headers.set(key, value);
452
- });
453
- return response;
454
- }
455
-
456
- // Health check endpoint (no auth required for Docker health checks)
457
- if (path === "/api/health") {
458
- const response = await handleApiRequest(req, path);
459
- Object.entries(corsHeaders).forEach(([key, value]) => {
460
- response.headers.set(key, value);
461
- });
462
- return response;
463
- }
464
-
465
- // Apply auth middleware
466
- const { response: authResponse, context } = await authMiddleware(req, path);
467
- if (authResponse) {
468
- Object.entries(corsHeaders).forEach(([key, value]) => {
469
- authResponse.headers.set(key, value);
470
- });
471
- return authResponse;
472
- }
473
-
474
- // Pass auth context to API handler
475
- const response = await handleApiRequest(req, path, context);
476
- Object.entries(corsHeaders).forEach(([key, value]) => {
477
- response.headers.set(key, value);
478
- });
479
- return response;
480
- }
481
-
482
- // Share routes (public, token-authenticated)
483
- if (path.startsWith("/share/")) {
484
- const shareResponse = await handleShareRequest(req, path);
485
- if (shareResponse) {
486
- Object.entries(corsHeaders).forEach(([key, value]) => {
487
- shareResponse.headers.set(key, value);
488
- });
489
- return shareResponse;
490
- }
491
- }
492
-
493
- // Serve static files (React app)
494
- return serveStatic(req, path);
495
- },
496
-
497
- // WebSocket proxy for agent voice/realtime
498
- websocket: {
499
- open(ws: any) {
500
- const { agentId, agentPort, agentApiKey } = ws.data;
501
- const wsTarget = `ws://localhost:${agentPort}/voice${agentApiKey ? `?api_key=${agentApiKey}` : ''}`;
502
- console.log(`[WS] Voice proxy connecting: agent=${agentId} port=${agentPort}`);
503
- const agentWs = new WebSocket(wsTarget);
504
-
505
- agentWs.onopen = () => {
506
- console.log(`[WS] Voice proxy connected: agent=${agentId} port=${agentPort}`);
507
- };
508
- agentWs.onmessage = (event: MessageEvent) => {
509
- try { ws.send(event.data); } catch {}
510
- };
511
- agentWs.onclose = (event: CloseEvent) => {
512
- console.log(`[WS] Agent disconnected: agent=${agentId} code=${event.code} reason=${event.reason}`);
513
- ws.close(event.code, event.reason);
514
- };
515
- agentWs.onerror = (err: any) => {
516
- console.log(`[WS] Agent WS error: agent=${agentId}`, err?.message || err);
517
- ws.close(1011, "Agent WebSocket error");
518
- };
519
-
520
- // Store agent WS on the client WS for message/close handlers
521
- ws.data.agentWs = agentWs;
522
- },
523
- message(ws: any, message: string | Buffer) {
524
- const agentWs = ws.data.agentWs as WebSocket;
525
- if (agentWs?.readyState === WebSocket.OPEN) {
526
- agentWs.send(message);
527
- }
528
- },
529
- close(ws: any, code: number, reason: string) {
530
- const agentWs = ws.data.agentWs as WebSocket;
531
- if (agentWs && agentWs.readyState !== WebSocket.CLOSED) {
532
- agentWs.close(code, reason);
533
- }
534
- console.log(`[WS] Client disconnected: agent=${ws.data.agentId} code=${code}`);
535
- },
536
- },
537
- });
538
-
539
- const serverUrl = `http://localhost:${PORT}`;
540
- console.log(`
541
- ${c.gray}Open${c.reset} ${c.blue}${c.bold}${link(serverUrl)}${c.reset}
542
- ${c.darkGray}Click link or Cmd/Ctrl+C to copy${c.reset}
543
- `);
544
-
545
- // Auto-restart agents, MCP servers, and channels that were running before restart
546
- const hasRestarts = agentsToRestart.length > 0 || mcpServersToRestart.length > 0 || channelsToRestart.length > 0;
547
-
548
- if (hasRestarts) {
549
- // Restart in background to not block startup
550
- (async () => {
551
- // Import startAgentProcess dynamically to avoid circular dependency
552
- const { startAgentProcess } = await import("./routes/api/agent-utils");
553
-
554
- // Restart MCP servers first (agents may depend on them)
555
- if (mcpServersToRestart.length > 0) {
556
- console.log(` ${c.darkGray}MCP${c.reset} ${c.gray}Restarting ${mcpServersToRestart.length} server(s)...${c.reset}`);
557
-
558
- for (const server of mcpServersToRestart) {
559
- try {
560
- // Helper to substitute $ENV_VAR references with actual values
561
- const substituteEnvVars = (str: string, env: Record<string, string>): string => {
562
- return str.replace(/\$([A-Z_][A-Z0-9_]*)/g, (_, varName) => {
563
- return env[varName] || '';
564
- });
565
- };
566
-
567
- let cmd: string[];
568
- const serverEnv = server.env || {};
569
-
570
- if (server.command) {
571
- cmd = server.command.split(" ");
572
- if (server.args) {
573
- const substitutedArgs = substituteEnvVars(server.args, serverEnv);
574
- cmd.push(...substitutedArgs.split(" "));
575
- }
576
- } else if (server.package) {
577
- cmd = ["npx", "-y", server.package];
578
- if (server.args) {
579
- const substitutedArgs = substituteEnvVars(server.args, serverEnv);
580
- cmd.push(...substitutedArgs.split(" "));
581
- }
582
- } else {
583
- console.log(` ${c.gray} ✗ ${server.name}: no command or package${c.reset}`);
584
- continue;
585
- }
586
-
587
- // Use permanently assigned port from DB, fallback to dynamic
588
- const port = server.port || await getNextPort();
589
- const result = await startMcpProcess(server.id, cmd, serverEnv, port);
590
-
591
- if (result.success) {
592
- McpServerDB.setStatus(server.id, "running", port);
593
- console.log(` ${c.gray} ✓ ${server.name} on :${port}${c.reset}`);
594
- } else {
595
- console.log(` ${c.gray} ✗ ${server.name}: ${result.error}${c.reset}`);
596
- }
597
- } catch (err) {
598
- console.log(` ${c.gray} ✗ ${server.name}: ${err}${c.reset}`);
599
- }
600
- }
601
- }
602
-
603
- // Then restart agents - in parallel
604
- if (agentsToRestart.length > 0) {
605
- console.log(` ${c.darkGray}Agents${c.reset} ${c.gray}Restarting ${agentsToRestart.length} agent(s)...${c.reset}`);
606
-
607
- await Promise.allSettled(agentsToRestart.map(async (agent) => {
608
- try {
609
- const result = await startAgentProcess(agent, { silent: true });
610
- if (result.success) {
611
- console.log(` ${c.gray} ✓ ${agent.name} on :${result.port}${c.reset}`);
612
- } else {
613
- console.log(` ${c.gray} ✗ ${agent.name}: ${result.error}${c.reset}`);
614
- }
615
- } catch (err) {
616
- console.log(` ${c.gray} ✗ ${agent.name}: ${err}${c.reset}`);
617
- }
618
- }));
619
- }
620
-
621
- // Restart channels (after agents, since channels depend on running agents)
622
- if (channelsToRestart.length > 0) {
623
- const { startChannel } = await import("./channels");
624
- console.log(` ${c.darkGray}Channels${c.reset} ${c.gray}Restarting ${channelsToRestart.length} channel(s)...${c.reset}`);
625
-
626
- await Promise.allSettled(channelsToRestart.map(async (channel) => {
627
- try {
628
- const result = await startChannel(channel.id);
629
- if (result.success) {
630
- console.log(` ${c.gray} ✓ ${channel.name} (${channel.type})${c.reset}`);
631
- } else {
632
- console.log(` ${c.gray} ✗ ${channel.name}: ${result.error}${c.reset}`);
633
- }
634
- } catch (err) {
635
- console.log(` ${c.gray} ✗ ${channel.name}: ${err}${c.reset}`);
636
- }
637
- }));
638
- }
639
- })();
640
- }
641
-
642
- // Note: Don't use "export default server" - it causes Bun to print "Started server" message