apteva 0.4.32 → 0.4.44
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/ActivityPage.c48n83h2.js +3 -0
- package/dist/ApiDocsPage.yzcxx5ax.js +4 -0
- package/dist/App.09yb8t0b.js +1 -0
- package/dist/App.152mbs1r.js +4 -0
- package/dist/App.3a67nx9w.js +4 -0
- package/dist/App.9epx6785.js +4 -0
- package/dist/App.d8955awp.js +4 -0
- package/dist/App.drwb57jq.js +4 -0
- package/dist/App.gssbmajb.js +4 -0
- package/dist/App.qw70pc29.js +53 -0
- package/dist/App.qzbx5wtj.js +4 -0
- package/dist/App.r5serxkt.js +8 -0
- package/dist/App.tpmp9020.js +20 -0
- package/dist/App.v2wb4d7d.js +61 -0
- package/dist/App.vxmaaj0m.js +13 -0
- package/dist/App.w4p2tda9.js +4 -0
- package/dist/App.wv2ng55q.js +221 -0
- package/dist/App.yncnrn0f.js +4 -0
- package/dist/ConnectionsPage.k6cspyqq.js +3 -0
- package/dist/McpPage.cdxm48xj.js +3 -0
- package/dist/SettingsPage.evpv7c2y.js +3 -0
- package/dist/SkillsPage.pvzp6c1a.js +3 -0
- package/dist/TasksPage.6jnvbpsy.js +3 -0
- package/dist/TelemetryPage.t7vk24zc.js +3 -0
- package/dist/TestsPage.5x6658aa.js +3 -0
- package/dist/ThreadsPage.3fvhtevh.js +3 -0
- package/dist/apteva-kit.css +1 -1
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +10 -9
- package/src/crypto.ts +4 -3
- package/src/db.ts +171 -36
- package/src/integrations/agentdojo.ts +95 -12
- package/src/integrations/index.ts +7 -0
- package/src/mcp-platform.ts +870 -142
- package/src/openapi.ts +96 -0
- package/src/providers.ts +60 -34
- package/src/routes/api/agent-utils.ts +59 -47
- package/src/routes/api/agents.ts +71 -2
- package/src/routes/api/integrations.ts +11 -5
- package/src/routes/api/mcp.ts +5 -4
- package/src/routes/api/meta-agent.ts +37 -1
- package/src/routes/api/projects.ts +3 -3
- package/src/routes/api/providers.ts +121 -30
- package/src/routes/api/skills.ts +2 -3
- package/src/routes/api/system.ts +98 -14
- package/src/routes/api/telemetry.ts +19 -1
- package/src/routes/share.ts +85 -0
- package/src/server.ts +43 -32
- package/src/triggers/agentdojo.ts +2 -2
- package/src/web/App.tsx +107 -21
- package/src/web/components/activity/ActivityPage.tsx +242 -389
- package/src/web/components/agents/AgentCard.tsx +19 -27
- package/src/web/components/agents/AgentPanel.tsx +358 -198
- package/src/web/components/agents/AgentsView.tsx +4 -4
- package/src/web/components/agents/CreateAgentModal.tsx +21 -79
- package/src/web/components/api/ApiDocsPage.tsx +66 -66
- package/src/web/components/auth/CreateAccountStep.tsx +16 -16
- package/src/web/components/auth/LoginPage.tsx +10 -10
- package/src/web/components/common/Icons.tsx +8 -0
- package/src/web/components/common/LoadingSpinner.tsx +2 -2
- package/src/web/components/common/Modal.tsx +8 -8
- package/src/web/components/common/Select.tsx +11 -10
- package/src/web/components/connections/ConnectionsPage.tsx +4 -4
- package/src/web/components/connections/IntegrationsTab.tsx +18 -18
- package/src/web/components/connections/OverviewTab.tsx +13 -13
- package/src/web/components/connections/TriggersTab.tsx +99 -99
- package/src/web/components/dashboard/Dashboard.tsx +177 -52
- package/src/web/components/index.ts +1 -1
- package/src/web/components/layout/Header.tsx +50 -34
- package/src/web/components/layout/Sidebar.tsx +41 -16
- package/src/web/components/mcp/IntegrationsPanel.tsx +160 -69
- package/src/web/components/mcp/McpPage.tsx +218 -209
- package/src/web/components/meta-agent/MetaAgent.tsx +15 -11
- package/src/web/components/onboarding/OnboardingWizard.tsx +25 -25
- package/src/web/components/settings/SettingsPage.tsx +389 -221
- package/src/web/components/skills/SkillsPage.tsx +88 -88
- package/src/web/components/tasks/TasksPage.tsx +385 -68
- package/src/web/components/telemetry/TelemetryPage.tsx +294 -39
- package/src/web/components/tests/TestsPage.tsx +50 -50
- package/src/web/components/threads/ThreadsPage.tsx +315 -0
- package/src/web/context/AuthContext.tsx +3 -3
- package/src/web/context/ProjectContext.tsx +8 -3
- package/src/web/context/TelemetryContext.tsx +24 -6
- package/src/web/context/ThemeContext.tsx +69 -0
- package/src/web/context/index.ts +3 -1
- package/src/web/styles.css +25 -7
- package/src/web/themes.ts +99 -0
- package/src/web/types.ts +4 -7
- package/dist/ActivityPage.41nbye4r.js +0 -3
- package/dist/ApiDocsPage.4smnt8m3.js +0 -4
- package/dist/App.0sbax9et.js +0 -4
- package/dist/App.0ws427h8.js +0 -4
- package/dist/App.6q6bar8b.js +0 -4
- package/dist/App.80301vdb.js +0 -4
- package/dist/App.af2wg84v.js +0 -267
- package/dist/App.ca1rz1ph.js +0 -4
- package/dist/App.ensa6z0r.js +0 -4
- package/dist/App.f8g7tych.js +0 -13
- package/dist/App.mvtqv6qc.js +0 -20
- package/dist/App.ncgc9cxy.js +0 -4
- package/dist/App.p02f4ret.js +0 -1
- package/dist/App.p0fb1pds.js +0 -4
- package/dist/App.pmaq48sj.js +0 -4
- package/dist/App.yv87t9m5.js +0 -4
- package/dist/App.zjmfm8p6.js +0 -4
- package/dist/ConnectionsPage.anb3rv9a.js +0 -3
- package/dist/McpPage.y396h6fy.js +0 -3
- package/dist/SettingsPage.p1hc60gk.js +0 -3
- package/dist/SkillsPage.yj3xdsay.js +0 -3
- package/dist/TasksPage.sjv0khtv.js +0 -3
- package/dist/TelemetryPage.2qm4w16r.js +0 -3
- package/dist/TestsPage.zzs4qfj8.js +0 -3
|
@@ -111,8 +111,11 @@ export async function handleIntegrationRoutes(
|
|
|
111
111
|
|
|
112
112
|
try {
|
|
113
113
|
const body = await req.json();
|
|
114
|
-
const { appSlug, redirectUrl, credentials
|
|
115
|
-
|
|
114
|
+
const { appSlug, redirectUrl, credentials } = body;
|
|
115
|
+
// Read project_id from query param (frontend sends it there), fall back to body
|
|
116
|
+
const url = new URL(req.url);
|
|
117
|
+
const projectId = url.searchParams.get("project_id") || body.project_id || null;
|
|
118
|
+
const apiKey = ProviderKeys.getDecryptedForProject(providerId, projectId);
|
|
116
119
|
if (!apiKey) {
|
|
117
120
|
return json({ error: `${provider.name} API key not configured` }, 401);
|
|
118
121
|
}
|
|
@@ -127,7 +130,8 @@ export async function handleIntegrationRoutes(
|
|
|
127
130
|
}
|
|
128
131
|
|
|
129
132
|
// Default redirect URL back to our integrations page
|
|
130
|
-
const
|
|
133
|
+
const origin = req.headers.get("origin") || process.env.INSTANCE_URL || `http://localhost:${process.env.PORT || 4280}`;
|
|
134
|
+
const callbackUrl = redirectUrl || `${origin}/mcp?tab=hosted&connected=${appSlug}`;
|
|
131
135
|
|
|
132
136
|
const result = await provider.initiateConnection(apiKey, user.id, appSlug, callbackUrl, credentials);
|
|
133
137
|
return json(result);
|
|
@@ -329,7 +333,8 @@ export async function handleIntegrationRoutes(
|
|
|
329
333
|
: mcpUrl;
|
|
330
334
|
|
|
331
335
|
// Check if already exists (match by config ID in URL)
|
|
332
|
-
|
|
336
|
+
// Use Light variant to skip expensive decryption of env/headers
|
|
337
|
+
const existing = McpServerDB.findAllLight().find(
|
|
333
338
|
s => s.source === "composio" && s.url?.includes(configId)
|
|
334
339
|
);
|
|
335
340
|
if (existing) {
|
|
@@ -527,8 +532,9 @@ export async function handleIntegrationRoutes(
|
|
|
527
532
|
console.log(`[agentdojo:add] fetched server: slug=${server.slug} name=${server.name} url=${server.url?.substring(0, 80)}`);
|
|
528
533
|
|
|
529
534
|
// Check if already exists — match by slug in URL, scoped to same project
|
|
535
|
+
// Use Light variant to skip expensive decryption of env/headers
|
|
530
536
|
const effectiveProjectId = projectId && projectId !== "unassigned" ? projectId : null;
|
|
531
|
-
const allServers = McpServerDB.
|
|
537
|
+
const allServers = McpServerDB.findAllLight();
|
|
532
538
|
const agentdojoServers = allServers.filter(s => s.source === "agentdojo");
|
|
533
539
|
console.log(`[agentdojo:add] total servers=${allServers.length} agentdojo servers=${agentdojoServers.length}`);
|
|
534
540
|
const existing = agentdojoServers.find(
|
package/src/routes/api/mcp.ts
CHANGED
|
@@ -28,16 +28,17 @@ export async function handleMcpRoutes(
|
|
|
28
28
|
let queryMode: string;
|
|
29
29
|
if (forAgent !== null) {
|
|
30
30
|
// Get servers available for an agent (global + agent's project)
|
|
31
|
-
|
|
31
|
+
// Use Light variant: skips expensive decryption of env/headers
|
|
32
|
+
servers = McpServerDB.findForAgentLight(forAgent || null);
|
|
32
33
|
queryMode = `forAgent=${forAgent}`;
|
|
33
34
|
} else if (projectFilter === "global") {
|
|
34
|
-
servers = McpServerDB.
|
|
35
|
+
servers = McpServerDB.findGlobalLight();
|
|
35
36
|
queryMode = "global";
|
|
36
37
|
} else if (projectFilter && projectFilter !== "all") {
|
|
37
|
-
servers = McpServerDB.
|
|
38
|
+
servers = McpServerDB.findByProjectLight(projectFilter);
|
|
38
39
|
queryMode = `project=${projectFilter}`;
|
|
39
40
|
} else {
|
|
40
|
-
servers = McpServerDB.
|
|
41
|
+
servers = McpServerDB.findAllLight();
|
|
41
42
|
queryMode = "all";
|
|
42
43
|
}
|
|
43
44
|
const agentdojoCount = servers.filter(s => s.source === "agentdojo").length;
|
|
@@ -56,6 +56,9 @@ WHAT YOU CAN DO:
|
|
|
56
56
|
- **Skills**: List, enable/disable, and assign skills to agents
|
|
57
57
|
- **Providers**: Check which LLM providers have API keys configured
|
|
58
58
|
- **Communication**: Send messages to running agents
|
|
59
|
+
- **Integrations**: Connect third-party apps (GitHub, Slack, etc.) via API key, create MCP servers from them, and add them locally
|
|
60
|
+
- **Tests**: Create and run automated tests for agent workflows
|
|
61
|
+
- **Triggers**: Subscribe agents to external events (webhooks)
|
|
59
62
|
|
|
60
63
|
WORKFLOW FOR CREATING AGENTS:
|
|
61
64
|
1. Use list_providers to check which providers have API keys
|
|
@@ -63,6 +66,14 @@ WORKFLOW FOR CREATING AGENTS:
|
|
|
63
66
|
3. Optionally assign MCP servers (for tools) and skills (for behavior)
|
|
64
67
|
4. Use start_agent to run it
|
|
65
68
|
|
|
69
|
+
WORKFLOW FOR ADDING INTEGRATIONS:
|
|
70
|
+
1. Use list_integration_providers to see available providers (agentdojo, composio)
|
|
71
|
+
2. Use list_integration_apps to browse available apps/toolkits
|
|
72
|
+
3. Use connect_integration_app to authenticate with an API key (for OAuth, direct user to Browse Toolkits UI)
|
|
73
|
+
4. Use create_integration_config to create an MCP server from the connected app
|
|
74
|
+
5. Use add_integration_config_locally to add it as a local MCP server
|
|
75
|
+
6. Use assign_mcp_server_to_agent to give an agent access
|
|
76
|
+
|
|
66
77
|
AGENT FEATURES (enable when creating/updating):
|
|
67
78
|
- **memory**: Persistent memory across conversations (needs OpenAI key for embeddings)
|
|
68
79
|
- **tasks**: Scheduling and task tracking
|
|
@@ -70,6 +81,9 @@ AGENT FEATURES (enable when creating/updating):
|
|
|
70
81
|
- **mcp**: Required if assigning MCP servers — gives the agent tool-use capability
|
|
71
82
|
- **files**: File read/write in agent workspace
|
|
72
83
|
|
|
84
|
+
CRITICAL — PROJECT CONTEXT:
|
|
85
|
+
Your chat context tells you the current project (name and id). **You MUST pass this project_id to EVERY tool call that accepts a project_id parameter.** API keys, integrations, MCP servers, and agents are scoped per project — calls without project_id will fail to find them. Extract the id from your context and always include it.
|
|
86
|
+
|
|
73
87
|
ALWAYS use your tools proactively. When a user says "create an agent", don't explain how — just do it. Confirm what you did after.
|
|
74
88
|
Be concise. Use markdown formatting.`,
|
|
75
89
|
features: {
|
|
@@ -81,6 +95,10 @@ Be concise. Use markdown formatting.`,
|
|
|
81
95
|
realtime: false,
|
|
82
96
|
files: false,
|
|
83
97
|
agents: false,
|
|
98
|
+
builtinTools: {
|
|
99
|
+
webSearch: providerId === "anthropic",
|
|
100
|
+
webFetch: providerId === "anthropic",
|
|
101
|
+
},
|
|
84
102
|
},
|
|
85
103
|
mcp_servers: [],
|
|
86
104
|
skills: [],
|
|
@@ -99,6 +117,8 @@ Be concise. Use markdown formatting.`,
|
|
|
99
117
|
port: metaAgent.port,
|
|
100
118
|
provider: metaAgent.provider,
|
|
101
119
|
model: metaAgent.model,
|
|
120
|
+
features: metaAgent.features,
|
|
121
|
+
systemPrompt: metaAgent.system_prompt,
|
|
102
122
|
},
|
|
103
123
|
});
|
|
104
124
|
}
|
|
@@ -118,8 +138,24 @@ Be concise. Use markdown formatting.`,
|
|
|
118
138
|
return json({ agent: toApiAgent(metaAgent), message: "Already running" });
|
|
119
139
|
}
|
|
120
140
|
|
|
141
|
+
// Ensure builtinTools are enabled for anthropic provider
|
|
142
|
+
if (metaAgent.provider === "anthropic" && !metaAgent.features?.builtinTools?.webSearch) {
|
|
143
|
+
const updatedFeatures = {
|
|
144
|
+
...metaAgent.features,
|
|
145
|
+
builtinTools: { webSearch: true, webFetch: true },
|
|
146
|
+
};
|
|
147
|
+
AgentDB.update(META_AGENT_ID, { features: updatedFeatures });
|
|
148
|
+
console.log(`[meta-agent] Enabled builtinTools for anthropic provider`);
|
|
149
|
+
}
|
|
150
|
+
// Always re-read to get latest features
|
|
151
|
+
const freshAgent = AgentDB.findById(META_AGENT_ID);
|
|
152
|
+
if (!freshAgent) {
|
|
153
|
+
return json({ error: "Meta agent not found after update" }, 500);
|
|
154
|
+
}
|
|
155
|
+
console.log(`[meta-agent] Starting with builtinTools:`, JSON.stringify(freshAgent.features?.builtinTools));
|
|
156
|
+
|
|
121
157
|
// Start the agent using existing startAgentProcess function
|
|
122
|
-
const result = await startAgentProcess(
|
|
158
|
+
const result = await startAgentProcess(freshAgent, { silent: true });
|
|
123
159
|
if (!result.success) {
|
|
124
160
|
return json({ error: result.error || "Failed to start meta agent" }, 500);
|
|
125
161
|
}
|
|
@@ -88,9 +88,9 @@ export async function handleProjectRoutes(
|
|
|
88
88
|
return json({ error: "Project not found" }, 404);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
// Stop any running agents in this project first
|
|
91
|
+
// Stop any running agents in this project first - in parallel
|
|
92
92
|
const projectAgents = AgentDB.findByProject(projectMatch[1]);
|
|
93
|
-
|
|
93
|
+
await Promise.allSettled(projectAgents.map(async (agent) => {
|
|
94
94
|
if (agent.status === "running") {
|
|
95
95
|
const entry = agentProcesses.get(agent.id);
|
|
96
96
|
if (entry) {
|
|
@@ -102,7 +102,7 @@ export async function handleProjectRoutes(
|
|
|
102
102
|
}
|
|
103
103
|
setAgentStatus(agent.id, "stopped", "project_deleted");
|
|
104
104
|
}
|
|
105
|
-
}
|
|
105
|
+
}));
|
|
106
106
|
|
|
107
107
|
ProjectDB.delete(projectMatch[1]);
|
|
108
108
|
return json({ success: true });
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
1
2
|
import { json, debug } from "./helpers";
|
|
3
|
+
|
|
4
|
+
// Detect if running inside a Docker container
|
|
5
|
+
async function isRunningInDocker(): Promise<boolean> {
|
|
6
|
+
try {
|
|
7
|
+
return existsSync("/.dockerenv") || existsSync("/run/.containerenv");
|
|
8
|
+
} catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
2
12
|
import { startAgentProcess, setAgentStatus } from "./agent-utils";
|
|
3
13
|
import { AgentDB, UserDB } from "../../db";
|
|
4
14
|
import { ProviderKeys, Onboarding, getProvidersWithStatus, PROVIDERS, type ProviderId } from "../../providers";
|
|
@@ -55,6 +65,7 @@ export async function handleProviderRoutes(
|
|
|
55
65
|
// GET /api/providers/ollama/status - Check if Ollama is running
|
|
56
66
|
if (path === "/api/providers/ollama/status" && method === "GET") {
|
|
57
67
|
const ollamaUrl = ProviderKeys.getDecrypted("ollama") || "http://localhost:11434";
|
|
68
|
+
const isDocker = await isRunningInDocker();
|
|
58
69
|
|
|
59
70
|
try {
|
|
60
71
|
const response = await fetch(`${ollamaUrl}/api/tags`, {
|
|
@@ -68,11 +79,96 @@ export async function handleProviderRoutes(
|
|
|
68
79
|
connected: true,
|
|
69
80
|
url: ollamaUrl,
|
|
70
81
|
modelCount: data.models?.length || 0,
|
|
82
|
+
isDocker,
|
|
71
83
|
});
|
|
72
84
|
}
|
|
73
|
-
return json({ connected: false, url: ollamaUrl, error: "Ollama not responding" });
|
|
85
|
+
return json({ connected: false, url: ollamaUrl, error: "Ollama not responding", isDocker });
|
|
74
86
|
} catch {
|
|
75
|
-
return json({ connected: false, url: ollamaUrl, error: "Ollama not reachable" });
|
|
87
|
+
return json({ connected: false, url: ollamaUrl, error: "Ollama not reachable", isDocker });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// POST /api/providers/ollama/install - Install Ollama automatically
|
|
92
|
+
if (path === "/api/providers/ollama/install" && method === "POST") {
|
|
93
|
+
// Don't allow install inside Docker containers
|
|
94
|
+
if (await isRunningInDocker()) {
|
|
95
|
+
return json({ success: false, error: "Cannot install Ollama inside a Docker container. Configure an external Ollama URL instead." }, 400);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Only supported on Linux/macOS
|
|
99
|
+
const platform = process.platform;
|
|
100
|
+
if (platform !== "linux" && platform !== "darwin") {
|
|
101
|
+
return json({ success: false, error: "Auto-install is only supported on Linux and macOS. Please download from https://ollama.com/download" }, 400);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check if already installed
|
|
105
|
+
try {
|
|
106
|
+
const which = Bun.spawnSync(["which", "ollama"]);
|
|
107
|
+
if (which.exitCode === 0) {
|
|
108
|
+
// Already installed, just make sure it's running
|
|
109
|
+
Bun.spawn(["ollama", "serve"], { stdout: "ignore", stderr: "ignore" });
|
|
110
|
+
// Wait a moment for it to start
|
|
111
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
112
|
+
return json({ success: true, message: "Ollama is already installed", alreadyInstalled: true });
|
|
113
|
+
}
|
|
114
|
+
} catch { /* not installed */ }
|
|
115
|
+
|
|
116
|
+
// Install Ollama using official install script
|
|
117
|
+
try {
|
|
118
|
+
const proc = Bun.spawnSync(["bash", "-c", "curl -fsSL https://ollama.com/install.sh | sh"], {
|
|
119
|
+
timeout: 120_000, // 2 minute timeout
|
|
120
|
+
stderr: "pipe",
|
|
121
|
+
stdout: "pipe",
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (proc.exitCode !== 0) {
|
|
125
|
+
const stderr = proc.stderr.toString().trim();
|
|
126
|
+
return json({ success: false, error: `Installation failed: ${stderr || "Unknown error"}` }, 500);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Start Ollama serve in background
|
|
130
|
+
Bun.spawn(["ollama", "serve"], { stdout: "ignore", stderr: "ignore" });
|
|
131
|
+
|
|
132
|
+
// Wait for it to come up
|
|
133
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
134
|
+
|
|
135
|
+
// Verify it's running
|
|
136
|
+
try {
|
|
137
|
+
const check = await fetch("http://localhost:11434/api/tags", { signal: AbortSignal.timeout(3000) });
|
|
138
|
+
if (check.ok) {
|
|
139
|
+
return json({ success: true, message: "Ollama installed and running" });
|
|
140
|
+
}
|
|
141
|
+
} catch { /* may still be starting */ }
|
|
142
|
+
|
|
143
|
+
return json({ success: true, message: "Ollama installed. It may take a moment to start." });
|
|
144
|
+
} catch (err: any) {
|
|
145
|
+
return json({ success: false, error: `Installation failed: ${err.message}` }, 500);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// POST /api/providers/ollama/pull - Pull a model
|
|
150
|
+
if (path === "/api/providers/ollama/pull" && method === "POST") {
|
|
151
|
+
const body = await req.json() as { model?: string };
|
|
152
|
+
const model = body.model;
|
|
153
|
+
if (!model) {
|
|
154
|
+
return json({ error: "Model name required" }, 400);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const ollamaUrl = ProviderKeys.getDecrypted("ollama") || "http://localhost:11434";
|
|
159
|
+
const response = await fetch(`${ollamaUrl}/api/pull`, {
|
|
160
|
+
method: "POST",
|
|
161
|
+
headers: { "Content-Type": "application/json" },
|
|
162
|
+
body: JSON.stringify({ name: model }),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (!response.ok) {
|
|
166
|
+
return json({ success: false, error: "Failed to start model pull" }, 500);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return json({ success: true, message: `Pulling ${model}...` });
|
|
170
|
+
} catch (err: any) {
|
|
171
|
+
return json({ success: false, error: `Failed to pull model: ${err.message}` }, 500);
|
|
76
172
|
}
|
|
77
173
|
}
|
|
78
174
|
|
|
@@ -216,38 +312,33 @@ export async function handleProviderRoutes(
|
|
|
216
312
|
a => a.status === "running" && a.provider === providerId
|
|
217
313
|
);
|
|
218
314
|
|
|
219
|
-
|
|
315
|
+
// Stop all agents first
|
|
220
316
|
for (const agent of runningAgents) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
agentProc.proc.kill();
|
|
226
|
-
agentProcesses.delete(agent.id);
|
|
227
|
-
}
|
|
228
|
-
setAgentStatus(agent.id, "stopped", "provider_restart");
|
|
229
|
-
|
|
230
|
-
// Wait a moment for port to be released
|
|
231
|
-
await new Promise(r => setTimeout(r, 500));
|
|
232
|
-
|
|
233
|
-
// Restart the agent with new key
|
|
234
|
-
const startResult = await startAgentProcess(agent, { silent: true });
|
|
235
|
-
restartResults.push({
|
|
236
|
-
id: agent.id,
|
|
237
|
-
name: agent.name,
|
|
238
|
-
success: startResult.success,
|
|
239
|
-
error: startResult.error,
|
|
240
|
-
});
|
|
241
|
-
} catch (e) {
|
|
242
|
-
restartResults.push({
|
|
243
|
-
id: agent.id,
|
|
244
|
-
name: agent.name,
|
|
245
|
-
success: false,
|
|
246
|
-
error: String(e),
|
|
247
|
-
});
|
|
317
|
+
const agentProc = agentProcesses.get(agent.id);
|
|
318
|
+
if (agentProc) {
|
|
319
|
+
agentProc.proc.kill();
|
|
320
|
+
agentProcesses.delete(agent.id);
|
|
248
321
|
}
|
|
322
|
+
setAgentStatus(agent.id, "stopped", "provider_restart");
|
|
249
323
|
}
|
|
250
324
|
|
|
325
|
+
// Wait once for ports to be released
|
|
326
|
+
if (runningAgents.length > 0) {
|
|
327
|
+
await new Promise(r => setTimeout(r, 500));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Restart all agents in parallel
|
|
331
|
+
const restartResults = await Promise.all(
|
|
332
|
+
runningAgents.map(async (agent) => {
|
|
333
|
+
try {
|
|
334
|
+
const startResult = await startAgentProcess(agent, { silent: true });
|
|
335
|
+
return { id: agent.id, name: agent.name, success: startResult.success, error: startResult.error };
|
|
336
|
+
} catch (e) {
|
|
337
|
+
return { id: agent.id, name: agent.name, success: false, error: String(e) };
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
);
|
|
341
|
+
|
|
251
342
|
return json({
|
|
252
343
|
success: true,
|
|
253
344
|
message: "API key saved successfully",
|
package/src/routes/api/skills.ts
CHANGED
|
@@ -499,13 +499,12 @@ export async function handleSkillRoutes(
|
|
|
499
499
|
const agentsWithSkill = AgentDB.findBySkill(skillMatch[1]);
|
|
500
500
|
const runningAgents = agentsWithSkill.filter(a => a.status === "running" && a.port);
|
|
501
501
|
|
|
502
|
-
|
|
502
|
+
await Promise.allSettled(runningAgents.map(async (agent) => {
|
|
503
503
|
try {
|
|
504
504
|
const providerKey = ProviderKeys.getDecrypted(agent.provider);
|
|
505
505
|
if (providerKey) {
|
|
506
506
|
const config = buildAgentConfig(agent, providerKey);
|
|
507
507
|
await pushConfigToAgent(agent.id, agent.port!, config);
|
|
508
|
-
// Push skills via /skills endpoint
|
|
509
508
|
if (config.skills?.definitions?.length > 0) {
|
|
510
509
|
await pushSkillsToAgent(agent.id, agent.port!, config.skills.definitions);
|
|
511
510
|
}
|
|
@@ -514,7 +513,7 @@ export async function handleSkillRoutes(
|
|
|
514
513
|
} catch (err) {
|
|
515
514
|
console.error(`Failed to push skill update to agent ${agent.name}:`, err);
|
|
516
515
|
}
|
|
517
|
-
}
|
|
516
|
+
}));
|
|
518
517
|
|
|
519
518
|
return json({ skill: updated, agents_updated: runningAgents.length });
|
|
520
519
|
} catch (err) {
|
package/src/routes/api/system.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { json } from "./helpers";
|
|
2
|
-
import { META_AGENT_ENABLED, fetchFromAgent, startAgentProcess, setAgentStatus } from "./agent-utils";
|
|
2
|
+
import { META_AGENT_ENABLED, fetchFromAgent, agentFetch, startAgentProcess, setAgentStatus } from "./agent-utils";
|
|
3
3
|
import { AgentDB } from "../../db";
|
|
4
4
|
import { ProviderKeys } from "../../providers";
|
|
5
5
|
import { agentProcesses, getBinaryStatus, BIN_DIR } from "../../server";
|
|
@@ -44,6 +44,7 @@ export async function handleSystemRoutes(
|
|
|
44
44
|
return json({
|
|
45
45
|
projects: process.env.PROJECTS_ENABLED === "true",
|
|
46
46
|
metaAgent: process.env.META_AGENT_ENABLED === "true",
|
|
47
|
+
costTracking: process.env.COST_TRACKING_ENABLED !== "false",
|
|
47
48
|
});
|
|
48
49
|
}
|
|
49
50
|
|
|
@@ -99,21 +100,16 @@ export async function handleSystemRoutes(
|
|
|
99
100
|
return json({ success: false, error: result.error }, 500);
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
// Restart agents that were running
|
|
103
|
-
const restartResults
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
103
|
+
// Restart agents that were running - in parallel
|
|
104
|
+
const restartResults = await Promise.all(
|
|
105
|
+
agentsToRestart.map(async (agentId) => {
|
|
106
|
+
const agent = AgentDB.findById(agentId);
|
|
107
|
+
if (!agent) return null;
|
|
107
108
|
console.log(`Restarting agent ${agent.name} after update...`);
|
|
108
109
|
const startResult = await startAgentProcess(agent);
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
success: startResult.success,
|
|
113
|
-
error: startResult.error,
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
}
|
|
110
|
+
return { id: agent.id, name: agent.name, success: startResult.success, error: startResult.error };
|
|
111
|
+
})
|
|
112
|
+
).then(r => r.filter(Boolean));
|
|
117
113
|
|
|
118
114
|
return json({
|
|
119
115
|
success: true,
|
|
@@ -185,6 +181,94 @@ export async function handleSystemRoutes(
|
|
|
185
181
|
return json({ task: { ...data, agentId: agent.id, agentName: agent.name } });
|
|
186
182
|
}
|
|
187
183
|
|
|
184
|
+
// POST /api/tasks/:agentId/:taskId/execute - Execute a task immediately
|
|
185
|
+
const executeTaskMatch = path.match(/^\/api\/tasks\/([^/]+)\/([^/]+)\/execute$/);
|
|
186
|
+
if (executeTaskMatch && method === "POST") {
|
|
187
|
+
const [, agentId, taskId] = executeTaskMatch;
|
|
188
|
+
const agent = AgentDB.findById(agentId);
|
|
189
|
+
if (!agent) return json({ error: "Agent not found" }, 404);
|
|
190
|
+
if (agent.status !== "running" || !agent.port) return json({ error: "Agent is not running" }, 400);
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const res = await agentFetch(agentId, agent.port, `/tasks/${taskId}/execute`, {
|
|
194
|
+
method: "POST",
|
|
195
|
+
signal: AbortSignal.timeout(5000),
|
|
196
|
+
});
|
|
197
|
+
const data = await res.json();
|
|
198
|
+
if (!res.ok) return json({ error: data.error || `HTTP ${res.status}` }, res.status);
|
|
199
|
+
return json(data);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
return json({ error: `Failed to execute task: ${err}` }, 500);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// POST /api/tasks/:agentId - Create a task on an agent
|
|
206
|
+
const createTaskMatch = path.match(/^\/api\/tasks\/([^/]+)$/);
|
|
207
|
+
if (createTaskMatch && method === "POST") {
|
|
208
|
+
const agentId = createTaskMatch[1];
|
|
209
|
+
const agent = AgentDB.findById(agentId);
|
|
210
|
+
if (!agent) return json({ error: "Agent not found" }, 404);
|
|
211
|
+
if (agent.status !== "running" || !agent.port) return json({ error: "Agent is not running" }, 400);
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const body = await req.json();
|
|
215
|
+
const res = await agentFetch(agentId, agent.port, "/tasks", {
|
|
216
|
+
method: "POST",
|
|
217
|
+
headers: { "Content-Type": "application/json" },
|
|
218
|
+
body: JSON.stringify(body),
|
|
219
|
+
signal: AbortSignal.timeout(5000),
|
|
220
|
+
});
|
|
221
|
+
const data = await res.json();
|
|
222
|
+
if (!res.ok) return json({ error: data.error || `HTTP ${res.status}` }, res.status);
|
|
223
|
+
return json(data, 201);
|
|
224
|
+
} catch (err) {
|
|
225
|
+
return json({ error: `Failed to create task: ${err}` }, 500);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// PUT /api/tasks/:agentId/:taskId - Update a task on an agent
|
|
230
|
+
if (singleTaskMatch && method === "PUT") {
|
|
231
|
+
const [, agentId, taskId] = singleTaskMatch;
|
|
232
|
+
const agent = AgentDB.findById(agentId);
|
|
233
|
+
if (!agent) return json({ error: "Agent not found" }, 404);
|
|
234
|
+
if (agent.status !== "running" || !agent.port) return json({ error: "Agent is not running" }, 400);
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const body = await req.json();
|
|
238
|
+
const res = await agentFetch(agentId, agent.port, `/tasks/${taskId}`, {
|
|
239
|
+
method: "PUT",
|
|
240
|
+
headers: { "Content-Type": "application/json" },
|
|
241
|
+
body: JSON.stringify(body),
|
|
242
|
+
signal: AbortSignal.timeout(5000),
|
|
243
|
+
});
|
|
244
|
+
const data = await res.json();
|
|
245
|
+
if (!res.ok) return json({ error: data.error || `HTTP ${res.status}` }, res.status);
|
|
246
|
+
return json(data);
|
|
247
|
+
} catch (err) {
|
|
248
|
+
return json({ error: `Failed to update task: ${err}` }, 500);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// DELETE /api/tasks/:agentId/:taskId - Delete a task on an agent
|
|
253
|
+
if (singleTaskMatch && method === "DELETE") {
|
|
254
|
+
const [, agentId, taskId] = singleTaskMatch;
|
|
255
|
+
const agent = AgentDB.findById(agentId);
|
|
256
|
+
if (!agent) return json({ error: "Agent not found" }, 404);
|
|
257
|
+
if (agent.status !== "running" || !agent.port) return json({ error: "Agent is not running" }, 400);
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const res = await agentFetch(agentId, agent.port, `/tasks/${taskId}`, {
|
|
261
|
+
method: "DELETE",
|
|
262
|
+
signal: AbortSignal.timeout(5000),
|
|
263
|
+
});
|
|
264
|
+
const data = await res.json();
|
|
265
|
+
if (!res.ok) return json({ error: data.error || `HTTP ${res.status}` }, res.status);
|
|
266
|
+
return json(data);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
return json({ error: `Failed to delete task: ${err}` }, 500);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
188
272
|
// GET /api/dashboard - Get dashboard statistics
|
|
189
273
|
if (path === "/api/dashboard" && method === "GET") {
|
|
190
274
|
const url = new URL(req.url);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { json } from "./helpers";
|
|
2
|
-
import { TelemetryDB } from "../../db";
|
|
2
|
+
import { TelemetryDB, AgentDB } from "../../db";
|
|
3
|
+
import { getModelCost } from "../../providers";
|
|
3
4
|
import { telemetryBroadcaster, type TelemetryEvent } from "../../server";
|
|
4
5
|
|
|
5
6
|
export async function handleTelemetryRoutes(
|
|
@@ -35,6 +36,23 @@ export async function handleTelemetryRoutes(
|
|
|
35
36
|
|
|
36
37
|
// Filter out debug events - too noisy
|
|
37
38
|
const filteredEvents = body.events.filter(e => e.level !== "debug");
|
|
39
|
+
|
|
40
|
+
// Compute cost per LLM event if cost tracking is enabled
|
|
41
|
+
const costTrackingEnabled = process.env.COST_TRACKING_ENABLED !== "false";
|
|
42
|
+
if (costTrackingEnabled) {
|
|
43
|
+
const agent = AgentDB.findById(body.agent_id);
|
|
44
|
+
if (agent) {
|
|
45
|
+
const pricing = getModelCost(agent.provider, agent.model);
|
|
46
|
+
for (const event of filteredEvents) {
|
|
47
|
+
if (event.category === "LLM" && event.data) {
|
|
48
|
+
const inputTokens = (event.data.input_tokens as number) || 0;
|
|
49
|
+
const outputTokens = (event.data.output_tokens as number) || 0;
|
|
50
|
+
(event as any).cost = (inputTokens * pricing.input_cost + outputTokens * pricing.output_cost) / 1_000_000;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
38
56
|
const inserted = TelemetryDB.insertBatch(body.agent_id, filteredEvents);
|
|
39
57
|
|
|
40
58
|
// Broadcast to SSE clients
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import { AgentDB, type Agent } from "../db";
|
|
3
|
+
import { agentFetch } from "./api/agent-utils";
|
|
4
|
+
|
|
5
|
+
function deriveShareToken(apiKey: string, agentId: string): string {
|
|
6
|
+
return createHash("sha256")
|
|
7
|
+
.update(apiKey + ":" + agentId + ":share")
|
|
8
|
+
.digest("hex")
|
|
9
|
+
.substring(0, 32);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function findAgentByShareToken(token: string): Agent | null {
|
|
13
|
+
const agents = AgentDB.findAll();
|
|
14
|
+
for (const agent of agents) {
|
|
15
|
+
const apiKey = AgentDB.getApiKey(agent.id);
|
|
16
|
+
if (!apiKey) continue;
|
|
17
|
+
if (deriveShareToken(apiKey, agent.id) === token) return agent;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Get the share token for an agent (used by API route for the UI) */
|
|
23
|
+
export function getShareToken(agentId: string): string | null {
|
|
24
|
+
const apiKey = AgentDB.getApiKey(agentId);
|
|
25
|
+
if (!apiKey) return null;
|
|
26
|
+
return deriveShareToken(apiKey, agentId);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function handleShareRequest(req: Request, path: string): Promise<Response | null> {
|
|
30
|
+
// Match /share/<32 hex chars> sub-paths for API calls
|
|
31
|
+
const infoMatch = path.match(/^\/share\/([a-f0-9]{32})\/info$/);
|
|
32
|
+
const chatMatch = path.match(/^\/share\/([a-f0-9]{32})\/chat$/);
|
|
33
|
+
|
|
34
|
+
if (!infoMatch && !chatMatch) return null;
|
|
35
|
+
|
|
36
|
+
const token = (infoMatch || chatMatch)![1];
|
|
37
|
+
const agent = findAgentByShareToken(token);
|
|
38
|
+
|
|
39
|
+
// Intentionally vague 404 — don't reveal whether token exists
|
|
40
|
+
if (!agent) {
|
|
41
|
+
return new Response("Not found", { status: 404 });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// GET /share/:token/info — agent info (no secrets)
|
|
45
|
+
if (infoMatch && req.method === "GET") {
|
|
46
|
+
return Response.json({
|
|
47
|
+
name: agent.name,
|
|
48
|
+
status: agent.status,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// POST /share/:token/chat — proxy to agent
|
|
53
|
+
if (chatMatch && req.method === "POST") {
|
|
54
|
+
if (agent.status !== "running" || !agent.port) {
|
|
55
|
+
return Response.json({ error: "Agent is currently offline" }, { status: 503 });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const body = await req.json();
|
|
60
|
+
const response = await agentFetch(agent.id, agent.port, "/chat", {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: { "Content-Type": "application/json" },
|
|
63
|
+
body: JSON.stringify(body),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
const errorText = await response.text();
|
|
68
|
+
return Response.json({ error: `Agent error: ${errorText}` }, { status: response.status });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return new Response(response.body, {
|
|
72
|
+
status: 200,
|
|
73
|
+
headers: {
|
|
74
|
+
"Content-Type": response.headers.get("Content-Type") || "text/event-stream",
|
|
75
|
+
"Cache-Control": "no-cache",
|
|
76
|
+
"Connection": "keep-alive",
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
} catch (err) {
|
|
80
|
+
return Response.json({ error: "Failed to connect to agent" }, { status: 500 });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return null;
|
|
85
|
+
}
|