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/src/server.ts CHANGED
@@ -114,8 +114,45 @@ const mcpServersToRestart = McpServerDB.findRunning();
114
114
  AgentDB.resetAllStatus();
115
115
  McpServerDB.resetAllStatus();
116
116
 
117
- // In-memory store for running agent processes only
118
- export const agentProcesses: Map<string, Subprocess> = new Map();
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 package or downloaded binary
127
- return getActualBinaryPath(BIN_DIR) || getBinaryPath(BIN_DIR);
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
- // Increment port counter
140
- export function getNextPort(): number {
141
- return nextAgentPort++;
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
- if (!confirm("Delete this thread?")) return;
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
- <p className="text-sm whitespace-pre-wrap">{msg.content}</p>
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
- if (!confirm("Clear all memories?")) return;
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
- if (!confirm("Delete this file?")) return;
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 fetch("/api/mcp/servers");
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.configured)
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
- <button
1020
- key={server.id}
1021
- type="button"
1022
- onClick={() => toggleMcpServer(server.id)}
1023
- className={`w-full flex items-center gap-3 p-3 rounded border text-left transition ${
1024
- form.mcpServers.includes(server.id)
1025
- ? "border-[#f97316] bg-[#f97316]/10"
1026
- : "border-[#222] hover:border-[#333]"
1027
- }`}
1028
- >
1029
- <div className={`w-2 h-2 rounded-full flex-shrink-0 ${
1030
- server.status === "running" ? "bg-green-400" : "bg-[#444]"
1031
- }`} />
1032
- <div className="flex-1 min-w-0">
1033
- <div className={`text-sm font-medium ${form.mcpServers.includes(server.id) ? "text-[#f97316]" : ""}`}>
1034
- {server.name}
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="text-xs text-[#666]">
1037
- {server.type} • {server.package || server.command || "custom"}
1038
- {server.status === "running" && server.port && ` • :${server.port}`}
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
- </div>
1041
- <div className={`text-xs px-2 py-0.5 rounded ${
1042
- server.status === "running"
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
- Only running servers will be connected to the agent.
1097
+ Remote servers are always available. Local servers must be running.
1052
1098
  </p>
1053
1099
  </div>
1054
1100
  )}