apteva 0.2.8 → 0.2.10
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/dist/App.44ge5b89.js +218 -0
- package/dist/index.html +2 -2
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/binary.ts +36 -36
- package/src/db.ts +130 -16
- package/src/integrations/composio.ts +437 -0
- package/src/integrations/index.ts +80 -0
- package/src/openapi.ts +1724 -0
- package/src/routes/api.ts +599 -107
- package/src/server.ts +82 -8
- package/src/web/App.tsx +3 -0
- package/src/web/components/agents/AgentPanel.tsx +84 -38
- package/src/web/components/api/ApiDocsPage.tsx +583 -0
- package/src/web/components/common/Icons.tsx +8 -0
- package/src/web/components/common/Modal.tsx +183 -0
- package/src/web/components/layout/Sidebar.tsx +7 -1
- package/src/web/components/mcp/IntegrationsPanel.tsx +743 -0
- package/src/web/components/mcp/McpPage.tsx +242 -83
- package/src/web/components/settings/SettingsPage.tsx +24 -9
- package/src/web/components/tasks/TasksPage.tsx +1 -1
- package/src/web/index.html +1 -1
- package/src/web/types.ts +4 -1
- package/dist/App.hzbfeg94.js +0 -217
package/src/server.ts
CHANGED
|
@@ -114,8 +114,45 @@ const mcpServersToRestart = McpServerDB.findRunning();
|
|
|
114
114
|
AgentDB.resetAllStatus();
|
|
115
115
|
McpServerDB.resetAllStatus();
|
|
116
116
|
|
|
117
|
-
//
|
|
118
|
-
|
|
117
|
+
// Clean up orphaned processes on agent ports (targeted cleanup based on DB)
|
|
118
|
+
async function cleanupOrphanedProcesses(): Promise<void> {
|
|
119
|
+
// Get all agents with assigned ports
|
|
120
|
+
const agents = AgentDB.findAll();
|
|
121
|
+
const assignedPorts = agents.map(a => a.port).filter((p): p is number => p !== null);
|
|
122
|
+
|
|
123
|
+
if (assignedPorts.length === 0) return;
|
|
124
|
+
|
|
125
|
+
let cleaned = 0;
|
|
126
|
+
for (const port of assignedPorts) {
|
|
127
|
+
try {
|
|
128
|
+
const res = await fetch(`http://localhost:${port}/health`, { signal: AbortSignal.timeout(200) });
|
|
129
|
+
if (res.ok) {
|
|
130
|
+
// Orphaned process on this port - shut it down
|
|
131
|
+
try {
|
|
132
|
+
await fetch(`http://localhost:${port}/shutdown`, { method: "POST", signal: AbortSignal.timeout(500) });
|
|
133
|
+
cleaned++;
|
|
134
|
+
} catch {
|
|
135
|
+
// Shutdown failed - will be handled when agent tries to start
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// Port not in use - good
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (cleaned > 0) {
|
|
144
|
+
console.log(` [cleanup] Stopped ${cleaned} orphaned agent process(es)`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Run cleanup (don't block startup)
|
|
149
|
+
cleanupOrphanedProcesses().catch(() => {});
|
|
150
|
+
|
|
151
|
+
// In-memory store for running agent processes (agent_id -> { process, port })
|
|
152
|
+
export const agentProcesses: Map<string, { proc: Subprocess; port: number }> = new Map();
|
|
153
|
+
|
|
154
|
+
// Track agents currently being started (to prevent race conditions)
|
|
155
|
+
export const agentsStarting: Set<string> = new Set();
|
|
119
156
|
|
|
120
157
|
// Binary path - can be overridden via environment variable, or found from npm/downloaded
|
|
121
158
|
export function getBinaryPathForAgent(): string {
|
|
@@ -123,8 +160,13 @@ export function getBinaryPathForAgent(): string {
|
|
|
123
160
|
if (process.env.AGENT_BINARY_PATH) {
|
|
124
161
|
return process.env.AGENT_BINARY_PATH;
|
|
125
162
|
}
|
|
126
|
-
// Otherwise use npm
|
|
127
|
-
|
|
163
|
+
// Otherwise use downloaded or npm binary (getActualBinaryPath checks both)
|
|
164
|
+
const actualPath = getActualBinaryPath(BIN_DIR);
|
|
165
|
+
if (actualPath) {
|
|
166
|
+
return actualPath;
|
|
167
|
+
}
|
|
168
|
+
// No binary found - return expected path for error messages
|
|
169
|
+
return getBinaryPath(BIN_DIR);
|
|
128
170
|
}
|
|
129
171
|
|
|
130
172
|
// Export for legacy compatibility
|
|
@@ -136,9 +178,41 @@ export { getBinaryStatus, BIN_DIR };
|
|
|
136
178
|
// Base port for spawned agents
|
|
137
179
|
export let nextAgentPort = 4100;
|
|
138
180
|
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
181
|
+
// Check if a port is available by trying to connect to it
|
|
182
|
+
async function isPortAvailable(port: number): Promise<boolean> {
|
|
183
|
+
try {
|
|
184
|
+
const controller = new AbortController();
|
|
185
|
+
const timeout = setTimeout(() => controller.abort(), 100);
|
|
186
|
+
try {
|
|
187
|
+
await fetch(`http://localhost:${port}/health`, { signal: controller.signal });
|
|
188
|
+
clearTimeout(timeout);
|
|
189
|
+
return false; // Port responded, something is running there
|
|
190
|
+
} catch (err: any) {
|
|
191
|
+
clearTimeout(timeout);
|
|
192
|
+
// Connection refused = port is available
|
|
193
|
+
// Abort error = port is available (timeout means nothing responded)
|
|
194
|
+
if (err?.code === "ECONNREFUSED" || err?.name === "AbortError") {
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
return true; // Assume available if we get other errors
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Get next available port (checking that nothing is using it)
|
|
205
|
+
export async function getNextPort(): Promise<number> {
|
|
206
|
+
const maxAttempts = 100; // Prevent infinite loop
|
|
207
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
208
|
+
const port = nextAgentPort++;
|
|
209
|
+
const available = await isPortAvailable(port);
|
|
210
|
+
if (available) {
|
|
211
|
+
return port;
|
|
212
|
+
}
|
|
213
|
+
console.log(`[port] Port ${port} in use, trying next...`);
|
|
214
|
+
}
|
|
215
|
+
throw new Error("Could not find available port after 100 attempts");
|
|
142
216
|
}
|
|
143
217
|
|
|
144
218
|
// ANSI color codes matching UI theme
|
|
@@ -331,7 +405,7 @@ if (hasRestarts) {
|
|
|
331
405
|
continue;
|
|
332
406
|
}
|
|
333
407
|
|
|
334
|
-
const port = getNextPort();
|
|
408
|
+
const port = await getNextPort();
|
|
335
409
|
const result = await startMcpProcess(server.id, cmd, serverEnv, port);
|
|
336
410
|
|
|
337
411
|
if (result.success) {
|
package/src/web/App.tsx
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
TelemetryPage,
|
|
29
29
|
LoginPage,
|
|
30
30
|
} from "./components";
|
|
31
|
+
import { ApiDocsPage } from "./components/api/ApiDocsPage";
|
|
31
32
|
|
|
32
33
|
function AppContent() {
|
|
33
34
|
// Auth state
|
|
@@ -265,6 +266,8 @@ function AppContent() {
|
|
|
265
266
|
{route === "mcp" && <McpPage />}
|
|
266
267
|
|
|
267
268
|
{route === "telemetry" && <TelemetryPage />}
|
|
269
|
+
|
|
270
|
+
{route === "api" && <ApiDocsPage />}
|
|
268
271
|
</main>
|
|
269
272
|
</div>
|
|
270
273
|
|
|
@@ -2,6 +2,8 @@ import React, { useState, useEffect } from "react";
|
|
|
2
2
|
import { Chat } from "@apteva/apteva-kit";
|
|
3
3
|
import { CloseIcon, MemoryIcon, TasksIcon, VisionIcon, OperatorIcon, McpIcon, RealtimeIcon, FilesIcon, MultiAgentIcon } from "../common/Icons";
|
|
4
4
|
import { Select } from "../common/Select";
|
|
5
|
+
import { useConfirm } from "../common/Modal";
|
|
6
|
+
import { useAuth } from "../../context";
|
|
5
7
|
import type { Agent, Provider, AgentFeatures, McpServer } from "../../types";
|
|
6
8
|
|
|
7
9
|
type Tab = "chat" | "threads" | "tasks" | "memory" | "files" | "settings";
|
|
@@ -150,6 +152,7 @@ function ThreadsTab({ agent }: { agent: Agent }) {
|
|
|
150
152
|
const [selectedThread, setSelectedThread] = useState<string | null>(null);
|
|
151
153
|
const [messages, setMessages] = useState<Array<{ role: string; content: string; created_at: string }>>([]);
|
|
152
154
|
const [loadingMessages, setLoadingMessages] = useState(false);
|
|
155
|
+
const { confirm, ConfirmDialog } = useConfirm();
|
|
153
156
|
|
|
154
157
|
// Reset state when agent changes
|
|
155
158
|
useEffect(() => {
|
|
@@ -200,7 +203,8 @@ function ThreadsTab({ agent }: { agent: Agent }) {
|
|
|
200
203
|
|
|
201
204
|
const deleteThread = async (threadId: string, e: React.MouseEvent) => {
|
|
202
205
|
e.stopPropagation();
|
|
203
|
-
|
|
206
|
+
const confirmed = await confirm("Delete this thread?", { confirmText: "Delete", title: "Delete Thread" });
|
|
207
|
+
if (!confirmed) return;
|
|
204
208
|
|
|
205
209
|
try {
|
|
206
210
|
await fetch(`/api/agents/${agent.id}/threads/${threadId}`, { method: "DELETE" });
|
|
@@ -242,6 +246,8 @@ function ThreadsTab({ agent }: { agent: Agent }) {
|
|
|
242
246
|
if (selectedThread) {
|
|
243
247
|
const selectedThreadData = threads.find(t => t.id === selectedThread);
|
|
244
248
|
return (
|
|
249
|
+
<>
|
|
250
|
+
{ConfirmDialog}
|
|
245
251
|
<div className="flex-1 flex flex-col overflow-hidden">
|
|
246
252
|
{/* Header with back button */}
|
|
247
253
|
<div className="flex items-center gap-3 px-4 py-3 border-b border-[#1a1a1a]">
|
|
@@ -284,7 +290,28 @@ function ThreadsTab({ agent }: { agent: Agent }) {
|
|
|
284
290
|
: "bg-[#1a1a1a] text-[#e0e0e0]"
|
|
285
291
|
}`}
|
|
286
292
|
>
|
|
287
|
-
<
|
|
293
|
+
<div className="text-sm whitespace-pre-wrap">
|
|
294
|
+
{typeof msg.content === "string"
|
|
295
|
+
? msg.content
|
|
296
|
+
: Array.isArray(msg.content)
|
|
297
|
+
? msg.content.map((block: any, j: number) => (
|
|
298
|
+
<div key={j}>
|
|
299
|
+
{block.type === "text" && block.text}
|
|
300
|
+
{block.type === "tool_use" && (
|
|
301
|
+
<div className="bg-[#222] p-2 rounded mt-1 text-xs text-[#888]">
|
|
302
|
+
🔧 Tool: {block.name}
|
|
303
|
+
</div>
|
|
304
|
+
)}
|
|
305
|
+
{block.type === "tool_result" && (
|
|
306
|
+
<div className="bg-[#222] p-2 rounded mt-1 text-xs text-[#888]">
|
|
307
|
+
📋 Result: {typeof block.content === "string" ? block.content.slice(0, 200) : "..."}
|
|
308
|
+
</div>
|
|
309
|
+
)}
|
|
310
|
+
</div>
|
|
311
|
+
))
|
|
312
|
+
: JSON.stringify(msg.content)
|
|
313
|
+
}
|
|
314
|
+
</div>
|
|
288
315
|
<p className="text-xs text-[#666] mt-1">
|
|
289
316
|
{new Date(msg.created_at).toLocaleTimeString()}
|
|
290
317
|
</p>
|
|
@@ -295,11 +322,14 @@ function ThreadsTab({ agent }: { agent: Agent }) {
|
|
|
295
322
|
)}
|
|
296
323
|
</div>
|
|
297
324
|
</div>
|
|
325
|
+
</>
|
|
298
326
|
);
|
|
299
327
|
}
|
|
300
328
|
|
|
301
329
|
// Show threads list (full width)
|
|
302
330
|
return (
|
|
331
|
+
<>
|
|
332
|
+
{ConfirmDialog}
|
|
303
333
|
<div className="flex-1 overflow-auto">
|
|
304
334
|
{threads.length === 0 ? (
|
|
305
335
|
<div className="flex items-center justify-center h-full text-[#666]">
|
|
@@ -333,6 +363,7 @@ function ThreadsTab({ agent }: { agent: Agent }) {
|
|
|
333
363
|
</div>
|
|
334
364
|
)}
|
|
335
365
|
</div>
|
|
366
|
+
</>
|
|
336
367
|
);
|
|
337
368
|
}
|
|
338
369
|
|
|
@@ -519,6 +550,7 @@ function MemoryTab({ agent }: { agent: Agent }) {
|
|
|
519
550
|
const [loading, setLoading] = useState(true);
|
|
520
551
|
const [error, setError] = useState<string | null>(null);
|
|
521
552
|
const [enabled, setEnabled] = useState(false);
|
|
553
|
+
const { confirm, ConfirmDialog } = useConfirm();
|
|
522
554
|
|
|
523
555
|
// Reset state when agent changes
|
|
524
556
|
useEffect(() => {
|
|
@@ -561,7 +593,8 @@ function MemoryTab({ agent }: { agent: Agent }) {
|
|
|
561
593
|
};
|
|
562
594
|
|
|
563
595
|
const clearAllMemories = async () => {
|
|
564
|
-
|
|
596
|
+
const confirmed = await confirm("Clear all memories?", { confirmText: "Clear", title: "Clear Memories" });
|
|
597
|
+
if (!confirmed) return;
|
|
565
598
|
try {
|
|
566
599
|
await fetch(`/api/agents/${agent.id}/memories`, { method: "DELETE" });
|
|
567
600
|
setMemories([]);
|
|
@@ -617,6 +650,8 @@ function MemoryTab({ agent }: { agent: Agent }) {
|
|
|
617
650
|
}
|
|
618
651
|
|
|
619
652
|
return (
|
|
653
|
+
<>
|
|
654
|
+
{ConfirmDialog}
|
|
620
655
|
<div className="flex-1 overflow-auto p-4">
|
|
621
656
|
<div className="flex items-center justify-between mb-4">
|
|
622
657
|
<h3 className="text-sm font-medium text-[#888]">Stored Memories ({memories.length})</h3>
|
|
@@ -672,6 +707,7 @@ function MemoryTab({ agent }: { agent: Agent }) {
|
|
|
672
707
|
</div>
|
|
673
708
|
)}
|
|
674
709
|
</div>
|
|
710
|
+
</>
|
|
675
711
|
);
|
|
676
712
|
}
|
|
677
713
|
|
|
@@ -691,6 +727,7 @@ function FilesTab({ agent }: { agent: Agent }) {
|
|
|
691
727
|
const [files, setFiles] = useState<AgentFile[]>([]);
|
|
692
728
|
const [loading, setLoading] = useState(true);
|
|
693
729
|
const [error, setError] = useState<string | null>(null);
|
|
730
|
+
const { confirm, ConfirmDialog } = useConfirm();
|
|
694
731
|
|
|
695
732
|
// Reset state when agent changes
|
|
696
733
|
useEffect(() => {
|
|
@@ -723,7 +760,8 @@ function FilesTab({ agent }: { agent: Agent }) {
|
|
|
723
760
|
}, [agent.id, agent.status]);
|
|
724
761
|
|
|
725
762
|
const deleteFile = async (fileId: string) => {
|
|
726
|
-
|
|
763
|
+
const confirmed = await confirm("Delete this file?", { confirmText: "Delete", title: "Delete File" });
|
|
764
|
+
if (!confirmed) return;
|
|
727
765
|
try {
|
|
728
766
|
await fetch(`/api/agents/${agent.id}/files/${fileId}`, { method: "DELETE" });
|
|
729
767
|
setFiles(prev => prev.filter(f => f.id !== fileId));
|
|
@@ -792,6 +830,8 @@ function FilesTab({ agent }: { agent: Agent }) {
|
|
|
792
830
|
}
|
|
793
831
|
|
|
794
832
|
return (
|
|
833
|
+
<>
|
|
834
|
+
{ConfirmDialog}
|
|
795
835
|
<div className="flex-1 overflow-auto p-4">
|
|
796
836
|
<div className="flex items-center justify-between mb-4">
|
|
797
837
|
<h3 className="text-sm font-medium text-[#888]">Agent Files ({files.length})</h3>
|
|
@@ -841,6 +881,7 @@ function FilesTab({ agent }: { agent: Agent }) {
|
|
|
841
881
|
</div>
|
|
842
882
|
)}
|
|
843
883
|
</div>
|
|
884
|
+
</>
|
|
844
885
|
);
|
|
845
886
|
}
|
|
846
887
|
|
|
@@ -850,6 +891,7 @@ function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
|
|
|
850
891
|
onUpdateAgent: (updates: Partial<Agent>) => Promise<{ error?: string }>;
|
|
851
892
|
onDeleteAgent: () => void;
|
|
852
893
|
}) {
|
|
894
|
+
const { authFetch } = useAuth();
|
|
853
895
|
const [form, setForm] = useState({
|
|
854
896
|
name: agent.name,
|
|
855
897
|
provider: agent.provider,
|
|
@@ -867,7 +909,7 @@ function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
|
|
|
867
909
|
useEffect(() => {
|
|
868
910
|
const fetchMcpServers = async () => {
|
|
869
911
|
try {
|
|
870
|
-
const res = await
|
|
912
|
+
const res = await authFetch("/api/mcp/servers");
|
|
871
913
|
const data = await res.json();
|
|
872
914
|
setAvailableMcpServers(data.servers || []);
|
|
873
915
|
} catch (e) {
|
|
@@ -875,7 +917,7 @@ function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
|
|
|
875
917
|
}
|
|
876
918
|
};
|
|
877
919
|
fetchMcpServers();
|
|
878
|
-
}, []);
|
|
920
|
+
}, [authFetch]);
|
|
879
921
|
|
|
880
922
|
// Reset form when agent changes
|
|
881
923
|
useEffect(() => {
|
|
@@ -893,7 +935,7 @@ function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
|
|
|
893
935
|
const selectedProvider = providers.find(p => p.id === form.provider);
|
|
894
936
|
|
|
895
937
|
const providerOptions = providers
|
|
896
|
-
.filter(p => p.
|
|
938
|
+
.filter(p => p.hasKey && p.type === "llm")
|
|
897
939
|
.map(p => ({ value: p.id, label: p.name }));
|
|
898
940
|
|
|
899
941
|
const modelOptions = selectedProvider?.models.map(m => ({
|
|
@@ -1015,40 +1057,44 @@ function SettingsTab({ agent, providers, onUpdateAgent, onDeleteAgent }: {
|
|
|
1015
1057
|
</p>
|
|
1016
1058
|
) : (
|
|
1017
1059
|
<div className="space-y-2">
|
|
1018
|
-
{availableMcpServers.map(server =>
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1060
|
+
{availableMcpServers.map(server => {
|
|
1061
|
+
const isRemote = server.type === "http" && server.url;
|
|
1062
|
+
const isAvailable = isRemote || server.status === "running";
|
|
1063
|
+
const serverInfo = isRemote
|
|
1064
|
+
? `${server.source || "remote"} • http`
|
|
1065
|
+
: `${server.type} • ${server.package || server.command || "custom"}${server.status === "running" && server.port ? ` • :${server.port}` : ""}`;
|
|
1066
|
+
return (
|
|
1067
|
+
<button
|
|
1068
|
+
key={server.id}
|
|
1069
|
+
type="button"
|
|
1070
|
+
onClick={() => toggleMcpServer(server.id)}
|
|
1071
|
+
className={`w-full flex items-center gap-3 p-3 rounded border text-left transition ${
|
|
1072
|
+
form.mcpServers.includes(server.id)
|
|
1073
|
+
? "border-[#f97316] bg-[#f97316]/10"
|
|
1074
|
+
: "border-[#222] hover:border-[#333]"
|
|
1075
|
+
}`}
|
|
1076
|
+
>
|
|
1077
|
+
<div className={`w-2 h-2 rounded-full flex-shrink-0 ${
|
|
1078
|
+
isAvailable ? "bg-green-400" : "bg-[#444]"
|
|
1079
|
+
}`} />
|
|
1080
|
+
<div className="flex-1 min-w-0">
|
|
1081
|
+
<div className={`text-sm font-medium ${form.mcpServers.includes(server.id) ? "text-[#f97316]" : ""}`}>
|
|
1082
|
+
{server.name}
|
|
1083
|
+
</div>
|
|
1084
|
+
<div className="text-xs text-[#666]">{serverInfo}</div>
|
|
1035
1085
|
</div>
|
|
1036
|
-
<div className=
|
|
1037
|
-
|
|
1038
|
-
|
|
1086
|
+
<div className={`text-xs px-2 py-0.5 rounded ${
|
|
1087
|
+
isAvailable
|
|
1088
|
+
? "bg-green-500/20 text-green-400"
|
|
1089
|
+
: "bg-[#222] text-[#666]"
|
|
1090
|
+
}`}>
|
|
1091
|
+
{isRemote ? "remote" : server.status}
|
|
1039
1092
|
</div>
|
|
1040
|
-
</
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
? "bg-green-500/20 text-green-400"
|
|
1044
|
-
: "bg-[#222] text-[#666]"
|
|
1045
|
-
}`}>
|
|
1046
|
-
{server.status}
|
|
1047
|
-
</div>
|
|
1048
|
-
</button>
|
|
1049
|
-
))}
|
|
1093
|
+
</button>
|
|
1094
|
+
);
|
|
1095
|
+
})}
|
|
1050
1096
|
<p className="text-xs text-[#666] mt-2">
|
|
1051
|
-
|
|
1097
|
+
Remote servers are always available. Local servers must be running.
|
|
1052
1098
|
</p>
|
|
1053
1099
|
</div>
|
|
1054
1100
|
)}
|