apteva 0.4.4 → 0.4.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.
package/src/server.ts CHANGED
@@ -421,7 +421,7 @@ if (hasRestarts) {
421
421
  // Restart in background to not block startup
422
422
  (async () => {
423
423
  // Import startAgentProcess dynamically to avoid circular dependency
424
- const { startAgentProcess } = await import("./routes/api");
424
+ const { startAgentProcess } = await import("./routes/api/agent-utils");
425
425
 
426
426
  // Restart MCP servers first (agents may depend on them)
427
427
  if (mcpServersToRestart.length > 0) {
@@ -51,6 +51,256 @@ const METHOD_COLORS: Record<string, string> = {
51
51
  patch: "#50e3c2",
52
52
  };
53
53
 
54
+ // Try It Out component
55
+ function TryItOut({
56
+ method,
57
+ path,
58
+ parameters,
59
+ requestBody,
60
+ authFetch,
61
+ }: {
62
+ method: string;
63
+ path: string;
64
+ parameters?: Array<{
65
+ name: string;
66
+ in: string;
67
+ required?: boolean;
68
+ schema?: { type: string };
69
+ }>;
70
+ requestBody?: any;
71
+ authFetch: (url: string, options?: RequestInit) => Promise<Response>;
72
+ }) {
73
+ const [paramValues, setParamValues] = useState<Record<string, string>>({});
74
+ const [bodyValue, setBodyValue] = useState("");
75
+ const [response, setResponse] = useState<{ status: number; data: any } | null>(null);
76
+ const [loading, setLoading] = useState(false);
77
+ const [error, setError] = useState<string | null>(null);
78
+
79
+ // Initialize body with example if available
80
+ useEffect(() => {
81
+ if (requestBody?.content?.["application/json"]?.schema) {
82
+ const schema = requestBody.content["application/json"].schema;
83
+ if (schema.example) {
84
+ setBodyValue(JSON.stringify(schema.example, null, 2));
85
+ } else if (schema.properties) {
86
+ // Generate example from properties
87
+ const example: Record<string, any> = {};
88
+ for (const [key, prop] of Object.entries(schema.properties) as [string, any][]) {
89
+ if (prop.example !== undefined) {
90
+ example[key] = prop.example;
91
+ } else if (prop.type === "string") {
92
+ example[key] = "";
93
+ } else if (prop.type === "number" || prop.type === "integer") {
94
+ example[key] = 0;
95
+ } else if (prop.type === "boolean") {
96
+ example[key] = false;
97
+ } else if (prop.type === "array") {
98
+ example[key] = [];
99
+ } else if (prop.type === "object") {
100
+ example[key] = {};
101
+ }
102
+ }
103
+ setBodyValue(JSON.stringify(example, null, 2));
104
+ }
105
+ }
106
+ }, [requestBody]);
107
+
108
+ const execute = async () => {
109
+ setLoading(true);
110
+ setError(null);
111
+ setResponse(null);
112
+
113
+ try {
114
+ // Build URL with path parameters
115
+ let url = path;
116
+ const queryParams: string[] = [];
117
+
118
+ for (const param of parameters || []) {
119
+ const value = paramValues[param.name] || "";
120
+ if (param.in === "path") {
121
+ url = url.replace(`{${param.name}}`, encodeURIComponent(value));
122
+ } else if (param.in === "query" && value) {
123
+ queryParams.push(`${param.name}=${encodeURIComponent(value)}`);
124
+ }
125
+ }
126
+
127
+ if (queryParams.length > 0) {
128
+ url += `?${queryParams.join("&")}`;
129
+ }
130
+
131
+ const options: RequestInit = {
132
+ method: method.toUpperCase(),
133
+ };
134
+
135
+ if (bodyValue && ["post", "put", "patch"].includes(method)) {
136
+ options.headers = { "Content-Type": "application/json" };
137
+ options.body = bodyValue;
138
+ }
139
+
140
+ const res = await authFetch(`/api${url}`, options);
141
+ let data;
142
+ const contentType = res.headers.get("content-type");
143
+ if (contentType?.includes("application/json")) {
144
+ data = await res.json();
145
+ } else {
146
+ data = await res.text();
147
+ }
148
+
149
+ setResponse({ status: res.status, data });
150
+ } catch (err: any) {
151
+ setError(err.message || "Request failed");
152
+ } finally {
153
+ setLoading(false);
154
+ }
155
+ };
156
+
157
+ const pathParams = parameters?.filter((p) => p.in === "path") || [];
158
+ const queryParams = parameters?.filter((p) => p.in === "query") || [];
159
+ const hasBody = ["post", "put", "patch"].includes(method) && requestBody;
160
+
161
+ return (
162
+ <div style={{ marginTop: 16, padding: 16, background: "#0a0a14", borderRadius: 6, border: "1px solid #222" }}>
163
+ <h4 style={{ fontSize: 13, color: "#f97316", marginBottom: 12, fontWeight: 600 }}>Try it out</h4>
164
+
165
+ {/* Path Parameters */}
166
+ {pathParams.length > 0 && (
167
+ <div style={{ marginBottom: 12 }}>
168
+ <div style={{ fontSize: 11, color: "#666", marginBottom: 6 }}>Path Parameters</div>
169
+ {pathParams.map((param) => (
170
+ <div key={param.name} style={{ marginBottom: 8 }}>
171
+ <label style={{ fontSize: 12, color: "#888", display: "block", marginBottom: 4 }}>
172
+ {param.name} {param.required && <span style={{ color: "#f66" }}>*</span>}
173
+ </label>
174
+ <input
175
+ type="text"
176
+ value={paramValues[param.name] || ""}
177
+ onChange={(e) => setParamValues({ ...paramValues, [param.name]: e.target.value })}
178
+ placeholder={param.schema?.type || "string"}
179
+ style={{
180
+ width: "100%",
181
+ padding: "8px 12px",
182
+ background: "#111",
183
+ border: "1px solid #333",
184
+ borderRadius: 4,
185
+ color: "#fff",
186
+ fontSize: 13,
187
+ fontFamily: "monospace",
188
+ }}
189
+ />
190
+ </div>
191
+ ))}
192
+ </div>
193
+ )}
194
+
195
+ {/* Query Parameters */}
196
+ {queryParams.length > 0 && (
197
+ <div style={{ marginBottom: 12 }}>
198
+ <div style={{ fontSize: 11, color: "#666", marginBottom: 6 }}>Query Parameters</div>
199
+ {queryParams.map((param) => (
200
+ <div key={param.name} style={{ marginBottom: 8 }}>
201
+ <label style={{ fontSize: 12, color: "#888", display: "block", marginBottom: 4 }}>
202
+ {param.name} {param.required && <span style={{ color: "#f66" }}>*</span>}
203
+ </label>
204
+ <input
205
+ type="text"
206
+ value={paramValues[param.name] || ""}
207
+ onChange={(e) => setParamValues({ ...paramValues, [param.name]: e.target.value })}
208
+ placeholder={param.schema?.type || "string"}
209
+ style={{
210
+ width: "100%",
211
+ padding: "8px 12px",
212
+ background: "#111",
213
+ border: "1px solid #333",
214
+ borderRadius: 4,
215
+ color: "#fff",
216
+ fontSize: 13,
217
+ fontFamily: "monospace",
218
+ }}
219
+ />
220
+ </div>
221
+ ))}
222
+ </div>
223
+ )}
224
+
225
+ {/* Request Body */}
226
+ {hasBody && (
227
+ <div style={{ marginBottom: 12 }}>
228
+ <div style={{ fontSize: 11, color: "#666", marginBottom: 6 }}>Request Body (JSON)</div>
229
+ <textarea
230
+ value={bodyValue}
231
+ onChange={(e) => setBodyValue(e.target.value)}
232
+ rows={6}
233
+ style={{
234
+ width: "100%",
235
+ padding: "8px 12px",
236
+ background: "#111",
237
+ border: "1px solid #333",
238
+ borderRadius: 4,
239
+ color: "#fff",
240
+ fontSize: 12,
241
+ fontFamily: "monospace",
242
+ resize: "vertical",
243
+ }}
244
+ />
245
+ </div>
246
+ )}
247
+
248
+ {/* Execute Button */}
249
+ <button
250
+ onClick={execute}
251
+ disabled={loading}
252
+ style={{
253
+ padding: "10px 20px",
254
+ background: loading ? "#333" : "#f97316",
255
+ color: loading ? "#666" : "#000",
256
+ border: "none",
257
+ borderRadius: 4,
258
+ cursor: loading ? "not-allowed" : "pointer",
259
+ fontSize: 13,
260
+ fontWeight: 600,
261
+ }}
262
+ >
263
+ {loading ? "Executing..." : "Execute"}
264
+ </button>
265
+
266
+ {/* Error */}
267
+ {error && (
268
+ <div style={{ marginTop: 12, padding: 12, background: "#2a1515", borderRadius: 4, color: "#f66", fontSize: 12 }}>
269
+ {error}
270
+ </div>
271
+ )}
272
+
273
+ {/* Response */}
274
+ {response && (
275
+ <div style={{ marginTop: 12 }}>
276
+ <div style={{ fontSize: 11, color: "#666", marginBottom: 6 }}>
277
+ Response{" "}
278
+ <span style={{ color: response.status >= 200 && response.status < 300 ? "#49cc90" : "#f66" }}>
279
+ {response.status}
280
+ </span>
281
+ </div>
282
+ <pre
283
+ style={{
284
+ padding: 12,
285
+ background: "#111",
286
+ borderRadius: 4,
287
+ color: "#888",
288
+ fontSize: 11,
289
+ fontFamily: "monospace",
290
+ overflow: "auto",
291
+ maxHeight: 300,
292
+ whiteSpace: "pre-wrap",
293
+ wordBreak: "break-word",
294
+ }}
295
+ >
296
+ {typeof response.data === "string" ? response.data : JSON.stringify(response.data, null, 2)}
297
+ </pre>
298
+ </div>
299
+ )}
300
+ </div>
301
+ );
302
+ }
303
+
54
304
  export function ApiDocsPage() {
55
305
  const { authFetch } = useAuth();
56
306
  const [spec, setSpec] = useState<OpenApiSpec | null>(null);
@@ -516,6 +766,15 @@ export function ApiDocsPage() {
516
766
  </div>
517
767
  </div>
518
768
  )}
769
+
770
+ {/* Try It Out */}
771
+ <TryItOut
772
+ method={method}
773
+ path={path}
774
+ parameters={details.parameters}
775
+ requestBody={details.requestBody}
776
+ authFetch={authFetch}
777
+ />
519
778
  </div>
520
779
  )}
521
780
  </div>
@@ -1,5 +1,6 @@
1
1
  import React, { useState, useEffect, useCallback, useMemo } from "react";
2
- import { useAgentActivity, useAuth, useProjects } from "../../context";
2
+ import { useAgentActivity, useAuth, useProjects, useTelemetryContext } from "../../context";
3
+ import type { TelemetryEvent } from "../../context";
3
4
  import type { Agent, Provider, Route, DashboardStats, Task } from "../../types";
4
5
 
5
6
  interface DashboardProps {
@@ -21,8 +22,10 @@ export function Dashboard({
21
22
  }: DashboardProps) {
22
23
  const { authFetch } = useAuth();
23
24
  const { currentProjectId } = useProjects();
25
+ const { events: realtimeEvents } = useTelemetryContext();
24
26
  const [stats, setStats] = useState<DashboardStats | null>(null);
25
27
  const [recentTasks, setRecentTasks] = useState<Task[]>([]);
28
+ const [historicalActivities, setHistoricalActivities] = useState<TelemetryEvent[]>([]);
26
29
 
27
30
  // Filter agents by current project
28
31
  const filteredAgents = useMemo(() => {
@@ -42,9 +45,10 @@ export function Dashboard({
42
45
 
43
46
  const fetchDashboardData = useCallback(async () => {
44
47
  try {
45
- const [dashRes, tasksRes] = await Promise.all([
48
+ const [dashRes, tasksRes, activityRes] = await Promise.all([
46
49
  authFetch("/api/dashboard"),
47
50
  authFetch("/api/tasks?status=all"),
51
+ authFetch("/api/telemetry/events?type=thread_activity&limit=20"),
48
52
  ]);
49
53
 
50
54
  if (dashRes.ok) {
@@ -56,6 +60,11 @@ export function Dashboard({
56
60
  const data = await tasksRes.json();
57
61
  setRecentTasks((data.tasks || []).slice(0, 5));
58
62
  }
63
+
64
+ if (activityRes.ok) {
65
+ const data = await activityRes.json();
66
+ setHistoricalActivities(data.events || []);
67
+ }
59
68
  } catch (e) {
60
69
  console.error("Failed to fetch dashboard data:", e);
61
70
  }
@@ -86,6 +95,36 @@ export function Dashboard({
86
95
  return { total, pending, running, completed };
87
96
  }, [stats, currentProjectId, filteredTasks]);
88
97
 
98
+ // Merge real-time + historical thread_activity events, deduplicate
99
+ const activities = useMemo(() => {
100
+ const realtimeActivities = realtimeEvents.filter(e => e.type === "thread_activity");
101
+ const seen = new Set(realtimeActivities.map(e => e.id));
102
+ const merged = [...realtimeActivities];
103
+ for (const evt of historicalActivities) {
104
+ if (!seen.has(evt.id)) {
105
+ merged.push(evt);
106
+ seen.add(evt.id);
107
+ }
108
+ }
109
+ // Filter by project
110
+ let filtered = merged;
111
+ if (currentProjectId) {
112
+ filtered = merged.filter(e => projectAgentIds.has(e.agent_id));
113
+ }
114
+ // Sort newest first
115
+ filtered.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
116
+ return filtered.slice(0, 8);
117
+ }, [realtimeEvents, historicalActivities, currentProjectId, projectAgentIds]);
118
+
119
+ // Build agent name lookup
120
+ const agentNameMap = useMemo(() => {
121
+ const map = new Map<string, string>();
122
+ for (const a of agents) {
123
+ map.set(a.id, a.name);
124
+ }
125
+ return map;
126
+ }, [agents]);
127
+
89
128
  return (
90
129
  <div className="flex-1 overflow-auto p-6">
91
130
  {/* Stats Cards */}
@@ -96,7 +135,7 @@ export function Dashboard({
96
135
  <StatCard label="Providers" value={configuredProviders.length} color="text-[#f97316]" />
97
136
  </div>
98
137
 
99
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
138
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
100
139
  {/* Agents List */}
101
140
  <DashboardCard
102
141
  title="Agents"
@@ -116,6 +155,31 @@ export function Dashboard({
116
155
  )}
117
156
  </DashboardCard>
118
157
 
158
+ {/* Activity Feed */}
159
+ <DashboardCard
160
+ title="Activity"
161
+ actionLabel="Telemetry"
162
+ onAction={() => onNavigate("telemetry")}
163
+ >
164
+ {activities.length === 0 ? (
165
+ <div className="p-4 text-center text-[#666]">
166
+ <p>No activity yet</p>
167
+ <p className="text-sm text-[#444] mt-1">Agent activity will appear here in real-time</p>
168
+ </div>
169
+ ) : (
170
+ <div className="divide-y divide-[#1a1a1a]">
171
+ {activities.map((evt) => (
172
+ <ActivityItem
173
+ key={evt.id}
174
+ activity={(evt.data?.activity as string) || "Working..."}
175
+ agentName={agentNameMap.get(evt.agent_id) || evt.agent_id}
176
+ timestamp={evt.timestamp}
177
+ />
178
+ ))}
179
+ </div>
180
+ )}
181
+ </DashboardCard>
182
+
119
183
  {/* Recent Tasks */}
120
184
  <DashboardCard
121
185
  title="Recent Tasks"
@@ -228,6 +292,31 @@ function AgentListItem({ agent, onSelect, showProject }: { agent: Agent; onSelec
228
292
  );
229
293
  }
230
294
 
295
+ function timeAgo(timestamp: string): string {
296
+ const seconds = Math.floor((Date.now() - new Date(timestamp).getTime()) / 1000);
297
+ if (seconds < 5) return "just now";
298
+ if (seconds < 60) return `${seconds}s ago`;
299
+ const minutes = Math.floor(seconds / 60);
300
+ if (minutes < 60) return `${minutes}m ago`;
301
+ const hours = Math.floor(minutes / 60);
302
+ if (hours < 24) return `${hours}h ago`;
303
+ const days = Math.floor(hours / 24);
304
+ return `${days}d ago`;
305
+ }
306
+
307
+ function ActivityItem({ activity, agentName, timestamp }: { activity: string; agentName: string; timestamp: string }) {
308
+ return (
309
+ <div className="px-4 py-3">
310
+ <p className="text-sm truncate">{activity}</p>
311
+ <div className="flex items-center gap-2 text-xs text-[#555] mt-1">
312
+ <span className="text-[#666]">{agentName}</span>
313
+ <span className="text-[#444]">&middot;</span>
314
+ <span>{timeAgo(timestamp)}</span>
315
+ </div>
316
+ </div>
317
+ );
318
+ }
319
+
231
320
  function TaskStatusBadge({ status }: { status: Task["status"] }) {
232
321
  const colors: Record<string, string> = {
233
322
  pending: "bg-yellow-500/20 text-yellow-400",
@@ -44,9 +44,11 @@ function hasMultipleAuthMethods(app: IntegrationApp): boolean {
44
44
  // Main component
45
45
  export function IntegrationsPanel({
46
46
  providerId = "composio",
47
+ projectId,
47
48
  onConnectionComplete,
48
49
  }: {
49
50
  providerId?: string;
51
+ projectId?: string | null;
50
52
  onConnectionComplete?: () => void;
51
53
  }) {
52
54
  const { authFetch } = useAuth();
@@ -80,10 +82,11 @@ export function IntegrationsPanel({
80
82
  const fetchData = useCallback(async () => {
81
83
  setLoading(true);
82
84
  setError(null);
85
+ const projectParam = projectId && projectId !== "unassigned" ? `?project_id=${projectId}` : "";
83
86
  try {
84
87
  const [appsRes, connectedRes] = await Promise.all([
85
- authFetch(`/api/integrations/${providerId}/apps`),
86
- authFetch(`/api/integrations/${providerId}/connected`),
88
+ authFetch(`/api/integrations/${providerId}/apps${projectParam}`),
89
+ authFetch(`/api/integrations/${providerId}/connected${projectParam}`),
87
90
  ]);
88
91
 
89
92
  const appsData = await appsRes.json();
@@ -96,7 +99,7 @@ export function IntegrationsPanel({
96
99
  setError("Failed to load integrations");
97
100
  }
98
101
  setLoading(false);
99
- }, [authFetch, providerId]);
102
+ }, [authFetch, providerId, projectId]);
100
103
 
101
104
  useEffect(() => {
102
105
  fetchData();
@@ -118,11 +121,12 @@ export function IntegrationsPanel({
118
121
  // Poll for pending connection status
119
122
  useEffect(() => {
120
123
  if (!pendingConnection?.connectionId) return;
124
+ const projectParam = projectId && projectId !== "unassigned" ? `?project_id=${projectId}` : "";
121
125
 
122
126
  const pollInterval = setInterval(async () => {
123
127
  try {
124
128
  const res = await authFetch(
125
- `/api/integrations/${providerId}/connection/${pendingConnection.connectionId}`
129
+ `/api/integrations/${providerId}/connection/${pendingConnection.connectionId}${projectParam}`
126
130
  );
127
131
  const data = await res.json();
128
132
 
@@ -142,7 +146,7 @@ export function IntegrationsPanel({
142
146
  }, 2000);
143
147
 
144
148
  return () => clearInterval(pollInterval);
145
- }, [pendingConnection, authFetch, providerId, fetchData, onConnectionComplete]);
149
+ }, [pendingConnection, authFetch, providerId, projectId, fetchData, onConnectionComplete]);
146
150
 
147
151
  // Initiate connection
148
152
  const connectApp = async (app: IntegrationApp, apiKey?: string, forceOAuth?: boolean) => {
@@ -172,7 +176,8 @@ export function IntegrationsPanel({
172
176
  };
173
177
  }
174
178
 
175
- const res = await authFetch(`/api/integrations/${providerId}/connect`, {
179
+ const projectParam = projectId && projectId !== "unassigned" ? `?project_id=${projectId}` : "";
180
+ const res = await authFetch(`/api/integrations/${providerId}/connect${projectParam}`, {
176
181
  method: "POST",
177
182
  headers: { "Content-Type": "application/json" },
178
183
  body: JSON.stringify(body),
@@ -231,9 +236,10 @@ export function IntegrationsPanel({
231
236
 
232
237
  // Disconnect (called after confirmation)
233
238
  const disconnectApp = async (account: ConnectedAccount) => {
239
+ const projectParam = projectId && projectId !== "unassigned" ? `?project_id=${projectId}` : "";
234
240
  try {
235
241
  const res = await authFetch(
236
- `/api/integrations/${providerId}/connection/${account.id}`,
242
+ `/api/integrations/${providerId}/connection/${account.id}${projectParam}`,
237
243
  { method: "DELETE" }
238
244
  );
239
245
 
@@ -263,7 +269,8 @@ export function IntegrationsPanel({
263
269
  setError(null);
264
270
 
265
271
  try {
266
- const res = await authFetch(`/api/integrations/${providerId}/configs`, {
272
+ const projectParam = projectId && projectId !== "unassigned" ? `?project_id=${projectId}` : "";
273
+ const res = await authFetch(`/api/integrations/${providerId}/configs${projectParam}`, {
267
274
  method: "POST",
268
275
  headers: { "Content-Type": "application/json" },
269
276
  body: JSON.stringify({