apteva 0.2.3 → 0.2.6

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 (38) hide show
  1. package/dist/App.0mzj9cz9.js +213 -0
  2. package/dist/index.html +1 -1
  3. package/dist/styles.css +1 -1
  4. package/package.json +6 -6
  5. package/src/binary.ts +271 -1
  6. package/src/crypto.ts +53 -0
  7. package/src/db.ts +492 -3
  8. package/src/mcp-client.ts +599 -0
  9. package/src/providers.ts +31 -0
  10. package/src/routes/api.ts +832 -64
  11. package/src/server.ts +169 -5
  12. package/src/web/App.tsx +44 -2
  13. package/src/web/components/agents/AgentCard.tsx +53 -9
  14. package/src/web/components/agents/AgentPanel.tsx +381 -0
  15. package/src/web/components/agents/AgentsView.tsx +27 -10
  16. package/src/web/components/agents/CreateAgentModal.tsx +7 -7
  17. package/src/web/components/agents/index.ts +1 -1
  18. package/src/web/components/common/Icons.tsx +8 -0
  19. package/src/web/components/common/Modal.tsx +2 -2
  20. package/src/web/components/common/Select.tsx +1 -1
  21. package/src/web/components/common/index.ts +1 -0
  22. package/src/web/components/dashboard/Dashboard.tsx +74 -25
  23. package/src/web/components/index.ts +5 -2
  24. package/src/web/components/layout/Sidebar.tsx +22 -2
  25. package/src/web/components/mcp/McpPage.tsx +1144 -0
  26. package/src/web/components/mcp/index.ts +1 -0
  27. package/src/web/components/onboarding/OnboardingWizard.tsx +5 -1
  28. package/src/web/components/settings/SettingsPage.tsx +312 -82
  29. package/src/web/components/tasks/TasksPage.tsx +129 -0
  30. package/src/web/components/tasks/index.ts +1 -0
  31. package/src/web/components/telemetry/TelemetryPage.tsx +359 -0
  32. package/src/web/context/TelemetryContext.tsx +202 -0
  33. package/src/web/context/index.ts +2 -0
  34. package/src/web/hooks/useAgents.ts +23 -0
  35. package/src/web/styles.css +18 -0
  36. package/src/web/types.ts +75 -1
  37. package/dist/App.wfhmfhx7.js +0 -213
  38. package/src/web/components/agents/ChatPanel.tsx +0 -63
package/src/server.ts CHANGED
@@ -4,8 +4,66 @@ import { serveStatic } from "./routes/static";
4
4
  import { join } from "path";
5
5
  import { homedir } from "os";
6
6
  import { mkdirSync, existsSync } from "fs";
7
- import { initDatabase, AgentDB, ProviderKeysDB } from "./db";
8
- import { ensureBinary, getBinaryPath, getBinaryStatus, getActualBinaryPath } from "./binary";
7
+ import { initDatabase, AgentDB, ProviderKeysDB, McpServerDB, type McpServer, type Agent } from "./db";
8
+ import { startMcpProcess } from "./mcp-client";
9
+ import {
10
+ ensureBinary,
11
+ getBinaryPath,
12
+ getBinaryStatus,
13
+ getActualBinaryPath,
14
+ initVersionTracking,
15
+ checkForUpdates,
16
+ getInstalledVersion,
17
+ getAptevaVersion,
18
+ downloadLatestBinary,
19
+ } from "./binary";
20
+
21
+ // ============ SSE Telemetry Broadcast ============
22
+ export interface TelemetryEvent {
23
+ id: string;
24
+ agent_id: string;
25
+ timestamp: string;
26
+ category: string;
27
+ type: string;
28
+ level: string;
29
+ trace_id?: string;
30
+ thread_id?: string;
31
+ data?: Record<string, unknown>;
32
+ duration_ms?: number;
33
+ error?: string;
34
+ }
35
+
36
+ class TelemetryBroadcaster {
37
+ private clients: Set<ReadableStreamDefaultController<string>> = new Set();
38
+
39
+ addClient(controller: ReadableStreamDefaultController<string>) {
40
+ this.clients.add(controller);
41
+ }
42
+
43
+ removeClient(controller: ReadableStreamDefaultController<string>) {
44
+ this.clients.delete(controller);
45
+ }
46
+
47
+ broadcast(events: TelemetryEvent[]) {
48
+ if (this.clients.size === 0) return;
49
+
50
+ const data = `data: ${JSON.stringify(events)}\n\n`;
51
+ for (const controller of this.clients) {
52
+ try {
53
+ controller.enqueue(data);
54
+ } catch {
55
+ // Client disconnected, remove it
56
+ this.clients.delete(controller);
57
+ }
58
+ }
59
+ }
60
+
61
+ get clientCount() {
62
+ return this.clients.size;
63
+ }
64
+ }
65
+
66
+ export const telemetryBroadcaster = new TelemetryBroadcaster();
9
67
 
10
68
  const PORT = parseInt(process.env.PORT || "4280");
11
69
 
@@ -33,8 +91,16 @@ if (await envFile.exists()) {
33
91
  // Initialize database (silently)
34
92
  initDatabase(DATA_DIR);
35
93
 
36
- // Reset all agents to stopped on startup (processes don't survive restart)
94
+ // Initialize version tracking
95
+ initVersionTracking(DATA_DIR);
96
+
97
+ // Get agents and MCP servers that were running before restart (for auto-restart)
98
+ const agentsToRestart = AgentDB.findRunning();
99
+ const mcpServersToRestart = McpServerDB.findRunning();
100
+
101
+ // Reset all agents and MCP servers to stopped on startup (processes don't survive restart)
37
102
  AgentDB.resetAllStatus();
103
+ McpServerDB.resetAllStatus();
38
104
 
39
105
  // In-memory store for running agent processes only
40
106
  export const agentProcesses: Map<string, Subprocess> = new Map();
@@ -86,8 +152,9 @@ function link(url: string, text?: string): string {
86
152
 
87
153
 
88
154
  // Startup banner
155
+ const aptevaVersion = getAptevaVersion();
89
156
  console.log(`
90
- ${c.orange}${c.bold}>_ apteva${c.reset}
157
+ ${c.orange}${c.bold}>_ apteva${c.reset} ${c.gray}v${aptevaVersion}${c.reset}
91
158
  ${c.gray}Run AI agents locally${c.reset}
92
159
  `);
93
160
 
@@ -97,9 +164,28 @@ const binaryResult = await ensureBinary(BIN_DIR);
97
164
  // ensureBinary prints its own status when downloading/failing
98
165
  // We only need to print "ready" if binary already existed
99
166
  if (binaryResult.success && !binaryResult.downloaded) {
100
- console.log(`${c.gray}binary ready${c.reset}`);
167
+ const installedVersion = getInstalledVersion();
168
+ console.log(`${c.gray}v${installedVersion || "unknown"} ready${c.reset}`);
101
169
  }
102
170
 
171
+ // Check for updates in background (don't block startup)
172
+ checkForUpdates().then(versions => {
173
+ const updates: string[] = [];
174
+ if (versions.apteva.updateAvailable) {
175
+ updates.push(`apteva: v${versions.apteva.installed} → v${versions.apteva.latest}`);
176
+ }
177
+ if (versions.agent.updateAvailable) {
178
+ updates.push(`agent: v${versions.agent.installed || "?"} → v${versions.agent.latest}`);
179
+ }
180
+ if (updates.length > 0) {
181
+ console.log(`\n ${c.orange}Updates available:${c.reset}`);
182
+ updates.forEach(u => console.log(` ${c.gray}• ${u}${c.reset}`));
183
+ console.log(` ${c.gray}Update from Settings or run: npx apteva@latest${c.reset}\n`);
184
+ }
185
+ }).catch(() => {
186
+ // Silently ignore version check failures
187
+ });
188
+
103
189
  // Check database
104
190
  process.stdout.write(` ${c.darkGray}Agents${c.reset} `);
105
191
  console.log(`${c.gray}${AgentDB.count()} loaded${c.reset}`);
@@ -151,4 +237,82 @@ console.log(`
151
237
  ${c.darkGray}Click link or Cmd/Ctrl+C to copy${c.reset}
152
238
  `);
153
239
 
240
+ // Auto-restart agents and MCP servers that were running before restart
241
+ const hasRestarts = agentsToRestart.length > 0 || mcpServersToRestart.length > 0;
242
+
243
+ if (hasRestarts) {
244
+ // Restart in background to not block startup
245
+ (async () => {
246
+ // Import startAgentProcess dynamically to avoid circular dependency
247
+ const { startAgentProcess } = await import("./routes/api");
248
+
249
+ // Restart MCP servers first (agents may depend on them)
250
+ if (mcpServersToRestart.length > 0) {
251
+ console.log(` ${c.darkGray}MCP${c.reset} ${c.gray}Restarting ${mcpServersToRestart.length} server(s)...${c.reset}`);
252
+
253
+ for (const server of mcpServersToRestart) {
254
+ try {
255
+ // Helper to substitute $ENV_VAR references with actual values
256
+ const substituteEnvVars = (str: string, env: Record<string, string>): string => {
257
+ return str.replace(/\$([A-Z_][A-Z0-9_]*)/g, (_, varName) => {
258
+ return env[varName] || '';
259
+ });
260
+ };
261
+
262
+ let cmd: string[];
263
+ const serverEnv = server.env || {};
264
+
265
+ if (server.command) {
266
+ cmd = server.command.split(" ");
267
+ if (server.args) {
268
+ const substitutedArgs = substituteEnvVars(server.args, serverEnv);
269
+ cmd.push(...substitutedArgs.split(" "));
270
+ }
271
+ } else if (server.package) {
272
+ cmd = ["npx", "-y", server.package];
273
+ if (server.args) {
274
+ const substitutedArgs = substituteEnvVars(server.args, serverEnv);
275
+ cmd.push(...substitutedArgs.split(" "));
276
+ }
277
+ } else {
278
+ console.log(` ${c.gray} ✗ ${server.name}: no command or package${c.reset}`);
279
+ continue;
280
+ }
281
+
282
+ const port = getNextPort();
283
+ const result = await startMcpProcess(server.id, cmd, serverEnv, port);
284
+
285
+ if (result.success) {
286
+ McpServerDB.setStatus(server.id, "running", port);
287
+ console.log(` ${c.gray} ✓ ${server.name} on :${port}${c.reset}`);
288
+ } else {
289
+ console.log(` ${c.gray} ✗ ${server.name}: ${result.error}${c.reset}`);
290
+ }
291
+ } catch (err) {
292
+ console.log(` ${c.gray} ✗ ${server.name}: ${err}${c.reset}`);
293
+ }
294
+ }
295
+ }
296
+
297
+ // Then restart agents
298
+ if (agentsToRestart.length > 0) {
299
+ console.log(` ${c.darkGray}Agents${c.reset} ${c.gray}Restarting ${agentsToRestart.length} agent(s)...${c.reset}`);
300
+
301
+ for (const agent of agentsToRestart) {
302
+ try {
303
+ const result = await startAgentProcess(agent, { silent: true });
304
+
305
+ if (result.success) {
306
+ console.log(` ${c.gray} ✓ ${agent.name} on :${result.port}${c.reset}`);
307
+ } else {
308
+ console.log(` ${c.gray} ✗ ${agent.name}: ${result.error}${c.reset}`);
309
+ }
310
+ } catch (err) {
311
+ console.log(` ${c.gray} ✗ ${agent.name}: ${err}${c.reset}`);
312
+ }
313
+ }
314
+ }
315
+ })();
316
+ }
317
+
154
318
  // Note: Don't use "export default server" - it causes Bun to print "Started server" message
package/src/web/App.tsx CHANGED
@@ -6,6 +6,9 @@ import "@apteva/apteva-kit/styles.css";
6
6
  import type { Agent, Provider, Route, NewAgentForm } from "./types";
7
7
  import { DEFAULT_FEATURES } from "./types";
8
8
 
9
+ // Context
10
+ import { TelemetryProvider } from "./context";
11
+
9
12
  // Hooks
10
13
  import { useAgents, useProviders, useOnboarding } from "./hooks";
11
14
 
@@ -20,6 +23,9 @@ import {
20
23
  CreateAgentModal,
21
24
  AgentsView,
22
25
  Dashboard,
26
+ TasksPage,
27
+ McpPage,
28
+ TelemetryPage,
23
29
  } from "./components";
24
30
 
25
31
  function App() {
@@ -33,6 +39,7 @@ function App() {
33
39
  runningCount,
34
40
  fetchAgents,
35
41
  createAgent,
42
+ updateAgent,
36
43
  deleteAgent,
37
44
  toggleAgent,
38
45
  } = useAgents(onboardingComplete === true);
@@ -48,6 +55,28 @@ function App() {
48
55
  const [selectedAgent, setSelectedAgent] = useState<Agent | null>(null);
49
56
  const [route, setRoute] = useState<Route>("dashboard");
50
57
  const [startError, setStartError] = useState<string | null>(null);
58
+ const [taskCount, setTaskCount] = useState(0);
59
+
60
+ // Fetch task count periodically
61
+ useEffect(() => {
62
+ if (onboardingComplete !== true) return;
63
+
64
+ const fetchTaskCount = async () => {
65
+ try {
66
+ const res = await fetch("/api/tasks?status=pending");
67
+ if (res.ok) {
68
+ const data = await res.json();
69
+ setTaskCount(data.count || 0);
70
+ }
71
+ } catch {
72
+ // Ignore errors
73
+ }
74
+ };
75
+
76
+ fetchTaskCount();
77
+ const interval = setInterval(fetchTaskCount, 15000);
78
+ return () => clearInterval(interval);
79
+ }, [onboardingComplete]);
51
80
 
52
81
  // Form state
53
82
  const [newAgent, setNewAgent] = useState<NewAgentForm>({
@@ -152,7 +181,7 @@ function App() {
152
181
  }
153
182
 
154
183
  return (
155
- <div className="min-h-screen bg-[#0a0a0a] text-[#e0e0e0] font-mono flex flex-col">
184
+ <div className="h-screen bg-[#0a0a0a] text-[#e0e0e0] font-mono flex flex-col overflow-hidden">
156
185
  <Header
157
186
  onNewAgent={() => setShowCreate(true)}
158
187
  canCreateAgent={configuredProviders.length > 0}
@@ -166,6 +195,7 @@ function App() {
166
195
  <Sidebar
167
196
  route={route}
168
197
  agentCount={agents.length}
198
+ taskCount={taskCount}
169
199
  onNavigate={handleNavigate}
170
200
  />
171
201
 
@@ -177,10 +207,12 @@ function App() {
177
207
  agents={agents}
178
208
  loading={loading}
179
209
  selectedAgent={selectedAgent}
210
+ providers={providers}
180
211
  onSelectAgent={handleSelectAgent}
181
212
  onCloseAgent={() => setSelectedAgent(null)}
182
213
  onToggleAgent={handleToggleAgent}
183
214
  onDeleteAgent={handleDeleteAgent}
215
+ onUpdateAgent={updateAgent}
184
216
  />
185
217
  )}
186
218
 
@@ -194,6 +226,12 @@ function App() {
194
226
  onSelectAgent={handleSelectAgent}
195
227
  />
196
228
  )}
229
+
230
+ {route === "tasks" && <TasksPage />}
231
+
232
+ {route === "mcp" && <McpPage />}
233
+
234
+ {route === "telemetry" && <TelemetryPage />}
197
235
  </main>
198
236
  </div>
199
237
 
@@ -218,4 +256,8 @@ function App() {
218
256
 
219
257
  // Mount the app
220
258
  const root = createRoot(document.getElementById("root")!);
221
- root.render(<App />);
259
+ root.render(
260
+ <TelemetryProvider>
261
+ <App />
262
+ </TelemetryProvider>
263
+ );
@@ -1,5 +1,6 @@
1
1
  import React from "react";
2
2
  import { MemoryIcon, TasksIcon, VisionIcon, OperatorIcon, McpIcon, RealtimeIcon } from "../common/Icons";
3
+ import { useAgentActivity } from "../../context";
3
4
  import type { Agent, AgentFeatures } from "../../types";
4
5
 
5
6
  interface AgentCardProps {
@@ -21,6 +22,8 @@ const FEATURE_ICONS: { key: keyof AgentFeatures; icon: React.ComponentType<{ cla
21
22
 
22
23
  export function AgentCard({ agent, selected, onSelect, onToggle, onDelete }: AgentCardProps) {
23
24
  const enabledFeatures = FEATURE_ICONS.filter(f => agent.features?.[f.key]);
25
+ const mcpServers = agent.mcpServerDetails || [];
26
+ const { isActive, category, type } = useAgentActivity(agent.id);
24
27
 
25
28
  return (
26
29
  <div
@@ -32,14 +35,27 @@ export function AgentCard({ agent, selected, onSelect, onToggle, onDelete }: Age
32
35
  }`}
33
36
  >
34
37
  <div className="flex items-start justify-between mb-3">
35
- <div>
36
- <h3 className="font-semibold text-lg">{agent.name}</h3>
37
- <p className="text-sm text-[#666]">
38
- {agent.provider} / {agent.model}
39
- {agent.port && <span className="text-[#444]"> · :{agent.port}</span>}
40
- </p>
38
+ <div className="flex items-center gap-2">
39
+ {/* Activity indicator */}
40
+ {agent.status === "running" && (
41
+ <span
42
+ className={`w-2 h-2 rounded-full transition-all ${
43
+ isActive
44
+ ? "bg-green-400 animate-pulse"
45
+ : "bg-[#333]"
46
+ }`}
47
+ title={isActive ? `${category}: ${type}` : "Idle"}
48
+ />
49
+ )}
50
+ <div>
51
+ <h3 className="font-semibold text-lg">{agent.name}</h3>
52
+ <p className="text-sm text-[#666]">
53
+ {agent.provider} / {agent.model}
54
+ {agent.port && <span className="text-[#444]"> · :{agent.port}</span>}
55
+ </p>
56
+ </div>
41
57
  </div>
42
- <StatusBadge status={agent.status} />
58
+ <StatusBadge status={agent.status} isActive={isActive && agent.status === "running"} activityType={type} />
43
59
  </div>
44
60
 
45
61
  {enabledFeatures.length > 0 && (
@@ -47,7 +63,7 @@ export function AgentCard({ agent, selected, onSelect, onToggle, onDelete }: Age
47
63
  {enabledFeatures.map(({ key, icon: Icon, label }) => (
48
64
  <span
49
65
  key={key}
50
- className="inline-flex items-center gap-1 px-2 py-0.5 rounded bg-[#1a1a1a] text-[#888] text-xs"
66
+ className="inline-flex items-center gap-1 px-2 py-0.5 rounded bg-[#f97316]/10 text-[#f97316]/70 text-xs"
51
67
  title={label}
52
68
  >
53
69
  <Icon className="w-3 h-3" />
@@ -57,6 +73,26 @@ export function AgentCard({ agent, selected, onSelect, onToggle, onDelete }: Age
57
73
  </div>
58
74
  )}
59
75
 
76
+ {/* MCP Servers */}
77
+ {mcpServers.length > 0 && (
78
+ <div className="flex flex-wrap gap-1.5 mb-3">
79
+ {mcpServers.map((server) => (
80
+ <span
81
+ key={server.id}
82
+ className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs ${
83
+ server.status === "running"
84
+ ? "bg-green-500/10 text-green-400"
85
+ : "bg-[#222] text-[#666]"
86
+ }`}
87
+ title={`MCP: ${server.name} (${server.status})`}
88
+ >
89
+ <McpIcon className="w-3 h-3" />
90
+ {server.name}
91
+ </span>
92
+ ))}
93
+ </div>
94
+ )}
95
+
60
96
  <p className="text-sm text-[#666] line-clamp-2 mb-4">
61
97
  {agent.systemPrompt}
62
98
  </p>
@@ -83,7 +119,15 @@ export function AgentCard({ agent, selected, onSelect, onToggle, onDelete }: Age
83
119
  );
84
120
  }
85
121
 
86
- function StatusBadge({ status }: { status: Agent["status"] }) {
122
+ function StatusBadge({ status, isActive, activityType }: { status: Agent["status"]; isActive?: boolean; activityType?: string }) {
123
+ if (status === "running" && isActive && activityType) {
124
+ return (
125
+ <span className="px-2 py-1 rounded text-xs font-medium bg-green-500/20 text-green-400 animate-pulse">
126
+ {activityType}
127
+ </span>
128
+ );
129
+ }
130
+
87
131
  return (
88
132
  <span
89
133
  className={`px-2 py-1 rounded text-xs font-medium ${