apteva 0.2.11 → 0.3.7

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
@@ -127,13 +127,26 @@ async function cleanupOrphanedProcesses(): Promise<void> {
127
127
  try {
128
128
  const res = await fetch(`http://localhost:${port}/health`, { signal: AbortSignal.timeout(200) });
129
129
  if (res.ok) {
130
- // Orphaned process on this port - shut it down
130
+ // Orphaned process on this port - shut it down gracefully first
131
131
  try {
132
132
  await fetch(`http://localhost:${port}/shutdown`, { method: "POST", signal: AbortSignal.timeout(500) });
133
- cleaned++;
133
+ await new Promise(r => setTimeout(r, 500)); // Wait for graceful shutdown
134
134
  } catch {
135
- // Shutdown failed - will be handled when agent tries to start
135
+ // Graceful shutdown failed
136
136
  }
137
+
138
+ // Check if still running and force kill if needed
139
+ try {
140
+ const check = await fetch(`http://localhost:${port}/health`, { signal: AbortSignal.timeout(200) });
141
+ if (check.ok) {
142
+ // Still running - force kill via lsof
143
+ const { execSync } = await import("child_process");
144
+ execSync(`lsof -ti :${port} | xargs -r kill -9 2>/dev/null || true`, { stdio: "ignore" });
145
+ }
146
+ } catch {
147
+ // Not responding anymore - good
148
+ }
149
+ cleaned++;
137
150
  }
138
151
  } catch {
139
152
  // Port not in use - good
@@ -154,6 +167,44 @@ export const agentProcesses: Map<string, { proc: Subprocess; port: number }> = n
154
167
  // Track agents currently being started (to prevent race conditions)
155
168
  export const agentsStarting: Set<string> = new Set();
156
169
 
170
+ // Graceful shutdown handler - stop all agents when server exits
171
+ async function shutdownAllAgents() {
172
+ if (agentProcesses.size === 0) return;
173
+
174
+ console.log(`\n Stopping ${agentProcesses.size} running agent(s)...`);
175
+
176
+ for (const [agentId, { proc, port }] of agentProcesses) {
177
+ try {
178
+ // Try graceful shutdown
179
+ await fetch(`http://localhost:${port}/shutdown`, {
180
+ method: "POST",
181
+ signal: AbortSignal.timeout(1000),
182
+ }).catch(() => {});
183
+
184
+ proc.kill();
185
+ AgentDB.setStatus(agentId, "stopped");
186
+ } catch {
187
+ // Ignore errors during shutdown
188
+ }
189
+ }
190
+ agentProcesses.clear();
191
+ }
192
+
193
+ // Handle process termination signals
194
+ let shuttingDown = false;
195
+ process.on("SIGINT", async () => {
196
+ if (shuttingDown) return;
197
+ shuttingDown = true;
198
+ await shutdownAllAgents();
199
+ process.exit(0);
200
+ });
201
+ process.on("SIGTERM", async () => {
202
+ if (shuttingDown) return;
203
+ shuttingDown = true;
204
+ await shutdownAllAgents();
205
+ process.exit(0);
206
+ });
207
+
157
208
  // Binary path - can be overridden via environment variable, or found from npm/downloaded
158
209
  export function getBinaryPathForAgent(): string {
159
210
  // Environment override takes priority
@@ -175,8 +226,8 @@ export const BINARY_PATH = getBinaryPathForAgent();
175
226
  // Export binary status function for API
176
227
  export { getBinaryStatus, BIN_DIR };
177
228
 
178
- // Base port for spawned agents
179
- export let nextAgentPort = 4100;
229
+ // Base port for MCP server proxies (separate range from agents which use 4100-4199)
230
+ export let nextMcpPort = 4200;
180
231
 
181
232
  // Check if a port is available by trying to connect to it
182
233
  async function isPortAvailable(port: number): Promise<boolean> {
@@ -201,11 +252,11 @@ async function isPortAvailable(port: number): Promise<boolean> {
201
252
  }
202
253
  }
203
254
 
204
- // Get next available port (checking that nothing is using it)
255
+ // Get next available port for MCP servers (checking that nothing is using it)
205
256
  export async function getNextPort(): Promise<number> {
206
257
  const maxAttempts = 100; // Prevent infinite loop
207
258
  for (let i = 0; i < maxAttempts; i++) {
208
- const port = nextAgentPort++;
259
+ const port = nextMcpPort++;
209
260
  const available = await isPortAvailable(port);
210
261
  if (available) {
211
262
  return port;
package/src/web/App.tsx CHANGED
@@ -25,10 +25,12 @@ import {
25
25
  Dashboard,
26
26
  TasksPage,
27
27
  McpPage,
28
+ SkillsPage,
28
29
  TelemetryPage,
29
30
  LoginPage,
30
31
  } from "./components";
31
32
  import { ApiDocsPage } from "./components/api/ApiDocsPage";
33
+ import { MetaAgentProvider, MetaAgentPanel } from "./components/meta-agent/MetaAgent";
32
34
 
33
35
  function AppContent() {
34
36
  // Auth state
@@ -63,6 +65,9 @@ function AppContent() {
63
65
  fetchProviders,
64
66
  } = useProviders(shouldFetchData);
65
67
 
68
+ // Filter to only LLM providers for agent creation
69
+ const llmProviders = configuredProviders.filter(p => p.type === "llm");
70
+
66
71
  // UI state
67
72
  const [showCreate, setShowCreate] = useState(false);
68
73
  const [selectedAgent, setSelectedAgent] = useState<Agent | null>(null);
@@ -99,12 +104,14 @@ function AppContent() {
99
104
  provider: "",
100
105
  systemPrompt: "You are a helpful assistant.",
101
106
  features: { ...DEFAULT_FEATURES },
107
+ mcpServers: [],
108
+ skills: [],
102
109
  });
103
110
 
104
111
  // Set default provider when providers are loaded
105
112
  useEffect(() => {
106
- if (configuredProviders.length > 0 && !newAgent.provider) {
107
- const defaultProvider = configuredProviders[0];
113
+ if (llmProviders.length > 0 && !newAgent.provider) {
114
+ const defaultProvider = llmProviders[0];
108
115
  const defaultModel = defaultProvider.models.find(m => m.recommended)?.value || defaultProvider.models[0]?.value || "";
109
116
  setNewAgent(prev => ({
110
117
  ...prev,
@@ -112,7 +119,7 @@ function AppContent() {
112
119
  model: defaultModel,
113
120
  }));
114
121
  }
115
- }, [configuredProviders, newAgent.provider]);
122
+ }, [llmProviders, newAgent.provider]);
116
123
 
117
124
  // Update selected agent when agents list changes
118
125
  useEffect(() => {
@@ -140,7 +147,7 @@ function AppContent() {
140
147
  if (!newAgent.name) return;
141
148
  await createAgent(newAgent);
142
149
  await refreshProjects(); // Refresh project agent counts
143
- const defaultProvider = configuredProviders[0];
150
+ const defaultProvider = llmProviders[0];
144
151
  const defaultModel = defaultProvider?.models.find(m => m.recommended)?.value || defaultProvider?.models[0]?.value || "";
145
152
  setNewAgent({
146
153
  name: "",
@@ -148,6 +155,8 @@ function AppContent() {
148
155
  provider: defaultProvider?.id || "",
149
156
  systemPrompt: "You are a helpful assistant.",
150
157
  features: { ...DEFAULT_FEATURES },
158
+ mcpServers: [],
159
+ skills: [],
151
160
  });
152
161
  setShowCreate(false);
153
162
  };
@@ -246,7 +255,7 @@ function AppContent() {
246
255
  onDeleteAgent={handleDeleteAgent}
247
256
  onUpdateAgent={updateAgent}
248
257
  onNewAgent={() => setShowCreate(true)}
249
- canCreateAgent={configuredProviders.length > 0}
258
+ canCreateAgent={llmProviders.length > 0}
250
259
  />
251
260
  )}
252
261
 
@@ -265,6 +274,8 @@ function AppContent() {
265
274
 
266
275
  {route === "mcp" && <McpPage />}
267
276
 
277
+ {route === "skills" && <SkillsPage />}
278
+
268
279
  {route === "telemetry" && <TelemetryPage />}
269
280
 
270
281
  {route === "api" && <ApiDocsPage />}
@@ -286,6 +297,9 @@ function AppContent() {
286
297
  }}
287
298
  />
288
299
  )}
300
+
301
+ {/* Meta Agent - side drawer */}
302
+ <MetaAgentPanel />
289
303
  </div>
290
304
  );
291
305
  }
@@ -295,9 +309,11 @@ function App() {
295
309
  return (
296
310
  <AuthProvider>
297
311
  <ProjectProvider>
298
- <TelemetryProvider>
299
- <AppContent />
300
- </TelemetryProvider>
312
+ <MetaAgentProvider>
313
+ <TelemetryProvider>
314
+ <AppContent />
315
+ </TelemetryProvider>
316
+ </MetaAgentProvider>
301
317
  </ProjectProvider>
302
318
  </AuthProvider>
303
319
  );
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { MemoryIcon, TasksIcon, VisionIcon, OperatorIcon, McpIcon, RealtimeIcon, FilesIcon, MultiAgentIcon } from "../common/Icons";
2
+ import { MemoryIcon, TasksIcon, VisionIcon, OperatorIcon, McpIcon, RealtimeIcon, FilesIcon, MultiAgentIcon, SkillsIcon } from "../common/Icons";
3
3
  import { useAgentActivity, useProjects } from "../../context";
4
4
  import type { Agent, AgentFeatures } from "../../types";
5
5
 
@@ -25,6 +25,7 @@ const FEATURE_ICONS: { key: keyof AgentFeatures; icon: React.ComponentType<{ cla
25
25
  export function AgentCard({ agent, selected, onSelect, onToggle, showProject }: AgentCardProps) {
26
26
  const enabledFeatures = FEATURE_ICONS.filter(f => agent.features?.[f.key]);
27
27
  const mcpServers = agent.mcpServerDetails || [];
28
+ const skills = agent.skillDetails || [];
28
29
  const { isActive, type } = useAgentActivity(agent.id);
29
30
  const { projects } = useProjects();
30
31
  const project = agent.projectId ? projects.find(p => p.id === agent.projectId) : null;
@@ -32,7 +33,7 @@ export function AgentCard({ agent, selected, onSelect, onToggle, showProject }:
32
33
  return (
33
34
  <div
34
35
  onClick={onSelect}
35
- className={`bg-[#111] rounded p-5 border transition cursor-pointer ${
36
+ className={`bg-[#111] rounded p-5 border transition cursor-pointer flex flex-col h-full ${
36
37
  selected
37
38
  ? 'border-[#f97316]'
38
39
  : 'border-[#1a1a1a] hover:border-[#333]'
@@ -73,30 +74,54 @@ export function AgentCard({ agent, selected, onSelect, onToggle, showProject }:
73
74
  {/* MCP Servers */}
74
75
  {mcpServers.length > 0 && (
75
76
  <div className="flex flex-wrap gap-1.5 mb-3">
76
- {mcpServers.map((server) => (
77
+ {mcpServers.map((server) => {
78
+ // HTTP/remote servers are always available
79
+ const isAvailable = (server.type === "http" && server.url) || server.status === "running";
80
+ return (
81
+ <span
82
+ key={server.id}
83
+ className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs ${
84
+ isAvailable
85
+ ? "bg-green-500/10 text-green-400"
86
+ : "bg-[#222] text-[#666]"
87
+ }`}
88
+ title={`MCP: ${server.name} (${isAvailable ? "available" : server.status})`}
89
+ >
90
+ <McpIcon className="w-3 h-3" />
91
+ {server.name}
92
+ </span>
93
+ );
94
+ })}
95
+ </div>
96
+ )}
97
+
98
+ {/* Skills */}
99
+ {skills.length > 0 && (
100
+ <div className="flex flex-wrap gap-1.5 mb-3">
101
+ {skills.map((skill) => (
77
102
  <span
78
- key={server.id}
103
+ key={skill.id}
79
104
  className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs ${
80
- server.status === "running"
81
- ? "bg-green-500/10 text-green-400"
105
+ skill.enabled
106
+ ? "bg-purple-500/10 text-purple-400"
82
107
  : "bg-[#222] text-[#666]"
83
108
  }`}
84
- title={`MCP: ${server.name} (${server.status})`}
109
+ title={`Skill: ${skill.name} v${skill.version}`}
85
110
  >
86
- <McpIcon className="w-3 h-3" />
87
- {server.name}
111
+ <SkillsIcon className="w-3 h-3" />
112
+ {skill.name}
88
113
  </span>
89
114
  ))}
90
115
  </div>
91
116
  )}
92
117
 
93
- <p className="text-sm text-[#666] line-clamp-2 mb-4">
118
+ <p className="text-sm text-[#666] line-clamp-2 mb-4 flex-1">
94
119
  {agent.systemPrompt}
95
120
  </p>
96
121
 
97
122
  <button
98
123
  onClick={onToggle}
99
- className={`w-full px-3 py-1.5 rounded text-sm font-medium transition ${
124
+ className={`w-full px-3 py-1.5 rounded text-sm font-medium transition mt-auto ${
100
125
  agent.status === "running"
101
126
  ? "bg-[#f97316]/20 text-[#f97316] hover:bg-[#f97316]/30"
102
127
  : "bg-[#3b82f6]/20 text-[#3b82f6] hover:bg-[#3b82f6]/30"