apteva 0.4.57 → 0.7.1
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/README.md +216 -54
- package/cli.js +35 -0
- package/install.js +92 -0
- package/package.json +15 -76
- package/LICENSE +0 -63
- package/bin/apteva.js +0 -196
- package/dist/ActivityPage.kxzzb4yc.js +0 -3
- package/dist/ApiDocsPage.zq998hbm.js +0 -4
- package/dist/App.55rea8mn.js +0 -61
- package/dist/App.5ywb23z4.js +0 -53
- package/dist/App.6thds120.js +0 -4
- package/dist/App.9tctxzqm.js +0 -8
- package/dist/App.a8r8ttaz.js +0 -4
- package/dist/App.agsv5bje.js +0 -4
- package/dist/App.cepapqmx.js +0 -4
- package/dist/App.dp041gb3.js +0 -221
- package/dist/App.fds72zb5.js +0 -4
- package/dist/App.fg9qj2dq.js +0 -4
- package/dist/App.ndfejbm9.js +0 -4
- package/dist/App.nxmfmq1h.js +0 -13
- package/dist/App.qdfyt8ba.js +0 -4
- package/dist/App.x2d0ygt6.js +0 -4
- package/dist/App.yt9p4nr3.js +0 -20
- package/dist/App.zn4mw16t.js +0 -1
- package/dist/ConnectionsPage.8r96ryw7.js +0 -3
- package/dist/McpPage.3cwh0gnd.js +0 -3
- package/dist/SettingsPage.ykgdh5ev.js +0 -3
- package/dist/SkillsPage.4np1s65b.js +0 -3
- package/dist/TasksPage.4g08t7p6.js +0 -3
- package/dist/TelemetryPage.72w9pwcp.js +0 -3
- package/dist/TestsPage.z4fk3r7r.js +0 -3
- package/dist/ThreadsPage.63tcajeh.js +0 -3
- package/dist/apteva-kit.css +0 -1
- package/dist/icon.png +0 -0
- package/dist/index.html +0 -16
- package/dist/styles.css +0 -1
- package/scripts/postinstall.mjs +0 -102
- package/src/auth/index.ts +0 -394
- package/src/auth/middleware.ts +0 -213
- package/src/binary.ts +0 -536
- package/src/channels/index.ts +0 -40
- package/src/channels/telegram.ts +0 -311
- package/src/crypto.ts +0 -301
- package/src/db-tests.ts +0 -174
- package/src/db.ts +0 -3133
- package/src/integrations/agentdojo.ts +0 -559
- package/src/integrations/composio.ts +0 -437
- package/src/integrations/index.ts +0 -87
- package/src/integrations/skillsmp.ts +0 -318
- package/src/mcp-client.ts +0 -605
- package/src/mcp-handler.ts +0 -394
- package/src/mcp-platform.ts +0 -2403
- package/src/openapi.ts +0 -2410
- package/src/providers.ts +0 -597
- package/src/routes/api/agent-utils.ts +0 -890
- package/src/routes/api/agents.ts +0 -916
- package/src/routes/api/api-keys.ts +0 -95
- package/src/routes/api/channels.ts +0 -182
- package/src/routes/api/helpers.ts +0 -12
- package/src/routes/api/integrations.ts +0 -639
- package/src/routes/api/mcp.ts +0 -574
- package/src/routes/api/meta-agent.ts +0 -195
- package/src/routes/api/projects.ts +0 -112
- package/src/routes/api/providers.ts +0 -424
- package/src/routes/api/skills.ts +0 -537
- package/src/routes/api/system.ts +0 -333
- package/src/routes/api/telemetry.ts +0 -203
- package/src/routes/api/tests.ts +0 -148
- package/src/routes/api/triggers.ts +0 -518
- package/src/routes/api/users.ts +0 -148
- package/src/routes/api/webhooks.ts +0 -171
- package/src/routes/api.ts +0 -53
- package/src/routes/auth.ts +0 -251
- package/src/routes/share.ts +0 -86
- package/src/routes/static.ts +0 -131
- package/src/server.ts +0 -642
- package/src/test-runner.ts +0 -598
- package/src/triggers/agentdojo.ts +0 -253
- package/src/triggers/composio.ts +0 -264
- package/src/triggers/index.ts +0 -71
- package/src/tui/AgentList.tsx +0 -145
- package/src/tui/App.tsx +0 -102
- package/src/tui/Login.tsx +0 -104
- package/src/tui/api.ts +0 -72
- package/src/tui/index.tsx +0 -7
- package/src/web/App.tsx +0 -455
- package/src/web/components/activity/ActivityPage.tsx +0 -314
- package/src/web/components/activity/index.ts +0 -1
- package/src/web/components/agents/AgentCard.tsx +0 -189
- package/src/web/components/agents/AgentPanel.tsx +0 -2244
- package/src/web/components/agents/AgentsView.tsx +0 -180
- package/src/web/components/agents/CreateAgentModal.tsx +0 -475
- package/src/web/components/agents/index.ts +0 -4
- package/src/web/components/api/ApiDocsPage.tsx +0 -842
- package/src/web/components/auth/CreateAccountStep.tsx +0 -176
- package/src/web/components/auth/LoginPage.tsx +0 -91
- package/src/web/components/auth/index.ts +0 -2
- package/src/web/components/common/Icons.tsx +0 -250
- package/src/web/components/common/LoadingSpinner.tsx +0 -44
- package/src/web/components/common/Modal.tsx +0 -199
- package/src/web/components/common/Select.tsx +0 -97
- package/src/web/components/common/index.ts +0 -20
- package/src/web/components/connections/ConnectionsPage.tsx +0 -54
- package/src/web/components/connections/IntegrationsTab.tsx +0 -170
- package/src/web/components/connections/OverviewTab.tsx +0 -137
- package/src/web/components/connections/TriggersTab.tsx +0 -1346
- package/src/web/components/dashboard/Dashboard.tsx +0 -572
- package/src/web/components/dashboard/index.ts +0 -1
- package/src/web/components/index.ts +0 -21
- package/src/web/components/layout/ErrorBanner.tsx +0 -18
- package/src/web/components/layout/Header.tsx +0 -332
- package/src/web/components/layout/Sidebar.tsx +0 -231
- package/src/web/components/layout/index.ts +0 -3
- package/src/web/components/mcp/IntegrationsPanel.tsx +0 -857
- package/src/web/components/mcp/McpPage.tsx +0 -2515
- package/src/web/components/mcp/index.ts +0 -1
- package/src/web/components/meta-agent/MetaAgent.tsx +0 -245
- package/src/web/components/onboarding/OnboardingWizard.tsx +0 -404
- package/src/web/components/onboarding/index.ts +0 -1
- package/src/web/components/settings/SettingsPage.tsx +0 -2776
- package/src/web/components/settings/index.ts +0 -1
- package/src/web/components/skills/SkillsPage.tsx +0 -1200
- package/src/web/components/tasks/TasksPage.tsx +0 -1116
- package/src/web/components/tasks/index.ts +0 -1
- package/src/web/components/telemetry/TelemetryPage.tsx +0 -1129
- package/src/web/components/tests/TestsPage.tsx +0 -594
- package/src/web/components/threads/ThreadsPage.tsx +0 -315
- package/src/web/context/AuthContext.tsx +0 -242
- package/src/web/context/ProjectContext.tsx +0 -214
- package/src/web/context/TelemetryContext.tsx +0 -299
- package/src/web/context/ThemeContext.tsx +0 -90
- package/src/web/context/UIModeContext.tsx +0 -49
- package/src/web/context/index.ts +0 -12
- package/src/web/hooks/index.ts +0 -3
- package/src/web/hooks/useAgents.ts +0 -115
- package/src/web/hooks/useOnboarding.ts +0 -20
- package/src/web/hooks/useProviders.ts +0 -75
- package/src/web/icon.png +0 -0
- package/src/web/index.html +0 -16
- package/src/web/styles.css +0 -118
- package/src/web/themes.ts +0 -162
- 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
|