apteva 0.4.3 → 0.4.5
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/App.y11xqt9m.js +227 -0
- package/dist/index.html +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/db.ts +93 -19
- package/src/integrations/agentdojo.ts +350 -0
- package/src/openapi.ts +195 -0
- package/src/providers.ts +78 -7
- package/src/routes/api/agent-utils.ts +638 -0
- package/src/routes/api/agents.ts +743 -0
- package/src/routes/api/helpers.ts +12 -0
- package/src/routes/api/integrations.ts +608 -0
- package/src/routes/api/mcp.ts +377 -0
- package/src/routes/api/meta-agent.ts +145 -0
- package/src/routes/api/projects.ts +95 -0
- package/src/routes/api/providers.ts +269 -0
- package/src/routes/api/skills.ts +538 -0
- package/src/routes/api/system.ts +215 -0
- package/src/routes/api/telemetry.ts +142 -0
- package/src/routes/api/users.ts +148 -0
- package/src/routes/api.ts +32 -3474
- package/src/server.ts +1 -1
- package/src/web/components/api/ApiDocsPage.tsx +259 -0
- package/src/web/components/mcp/IntegrationsPanel.tsx +15 -8
- package/src/web/components/mcp/McpPage.tsx +458 -174
- package/src/web/components/settings/SettingsPage.tsx +275 -36
- package/src/web/components/skills/SkillsPage.tsx +330 -1
- package/src/web/components/tasks/TasksPage.tsx +187 -58
- package/src/web/context/TelemetryContext.tsx +14 -1
- package/src/web/hooks/useAgents.ts +9 -0
- package/src/web/types.ts +22 -4
- package/dist/App.mbp9atpm.js +0 -227
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
import { json } from "./helpers";
|
|
2
|
+
import { AgentDB, SkillDB, type Skill } from "../../db";
|
|
3
|
+
import { ProviderKeys } from "../../providers";
|
|
4
|
+
import { SkillsmpProvider, parseSkillMd } from "../../integrations/skillsmp";
|
|
5
|
+
import { buildAgentConfig, pushConfigToAgent, pushSkillsToAgent } from "./agent-utils";
|
|
6
|
+
|
|
7
|
+
export async function handleSkillRoutes(
|
|
8
|
+
req: Request,
|
|
9
|
+
path: string,
|
|
10
|
+
method: string,
|
|
11
|
+
): Promise<Response | null> {
|
|
12
|
+
// ============ Skills CRUD ============
|
|
13
|
+
|
|
14
|
+
// GET /api/skills - List skills (optionally filtered by project)
|
|
15
|
+
if (path === "/api/skills" && method === "GET") {
|
|
16
|
+
const url = new URL(req.url);
|
|
17
|
+
const projectFilter = url.searchParams.get("project"); // "all", "global", or project ID
|
|
18
|
+
const forAgent = url.searchParams.get("forAgent"); // agent's project ID (shows global + project)
|
|
19
|
+
|
|
20
|
+
let skills;
|
|
21
|
+
if (forAgent !== null) {
|
|
22
|
+
// Get skills available for an agent (global + agent's project)
|
|
23
|
+
skills = SkillDB.findForAgent(forAgent || null);
|
|
24
|
+
} else if (projectFilter === "global") {
|
|
25
|
+
skills = SkillDB.findGlobal();
|
|
26
|
+
} else if (projectFilter && projectFilter !== "all") {
|
|
27
|
+
skills = SkillDB.findByProject(projectFilter);
|
|
28
|
+
} else {
|
|
29
|
+
skills = SkillDB.findAll();
|
|
30
|
+
}
|
|
31
|
+
return json({ skills });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// POST /api/skills - Create a new skill
|
|
35
|
+
if (path === "/api/skills" && method === "POST") {
|
|
36
|
+
try {
|
|
37
|
+
const body = await req.json();
|
|
38
|
+
const { name, description, content, version, license, compatibility, metadata, allowed_tools, source, source_url, enabled, project_id } = body;
|
|
39
|
+
|
|
40
|
+
if (!name || !description || !content) {
|
|
41
|
+
return json({ error: "name, description, and content are required" }, 400);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Validate name format (lowercase, hyphens only)
|
|
45
|
+
if (!/^[a-z][a-z0-9-]*[a-z0-9]$|^[a-z]$/.test(name)) {
|
|
46
|
+
return json({ error: "name must be lowercase letters, numbers, and hyphens only" }, 400);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (SkillDB.exists(name)) {
|
|
50
|
+
return json({ error: "A skill with this name already exists" }, 400);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const skill = SkillDB.create({
|
|
54
|
+
name,
|
|
55
|
+
description,
|
|
56
|
+
content,
|
|
57
|
+
version: version || "1.0.0",
|
|
58
|
+
license: license || null,
|
|
59
|
+
compatibility: compatibility || null,
|
|
60
|
+
metadata: metadata || {},
|
|
61
|
+
allowed_tools: allowed_tools || [],
|
|
62
|
+
source: source || "local",
|
|
63
|
+
source_url: source_url || null,
|
|
64
|
+
enabled: enabled !== false,
|
|
65
|
+
project_id: project_id || null,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return json({ skill }, 201);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.error("Failed to create skill:", err);
|
|
71
|
+
return json({ error: `Failed to create skill: ${err}` }, 500);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// POST /api/skills/import - Import a skill from SKILL.md content
|
|
76
|
+
if (path === "/api/skills/import" && method === "POST") {
|
|
77
|
+
try {
|
|
78
|
+
const body = await req.json();
|
|
79
|
+
const { content, source, source_url } = body;
|
|
80
|
+
|
|
81
|
+
if (!content) {
|
|
82
|
+
return json({ error: "content is required" }, 400);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const parsed = parseSkillMd(content);
|
|
86
|
+
if (!parsed) {
|
|
87
|
+
return json({ error: "Invalid SKILL.md format. Must have YAML frontmatter with name and description." }, 400);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (SkillDB.exists(parsed.name)) {
|
|
91
|
+
return json({ error: `A skill named "${parsed.name}" already exists` }, 400);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const skill = SkillDB.create({
|
|
95
|
+
name: parsed.name,
|
|
96
|
+
description: parsed.description,
|
|
97
|
+
content: content, // Store full content including frontmatter
|
|
98
|
+
version: (parsed as any).version || "1.0.0",
|
|
99
|
+
license: parsed.license || null,
|
|
100
|
+
compatibility: parsed.compatibility || null,
|
|
101
|
+
metadata: parsed.metadata || {},
|
|
102
|
+
allowed_tools: parsed.allowedTools || [],
|
|
103
|
+
source: source || "import",
|
|
104
|
+
source_url: source_url || null,
|
|
105
|
+
enabled: true,
|
|
106
|
+
project_id: null,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return json({ skill }, 201);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.error("Failed to import skill:", err);
|
|
112
|
+
return json({ error: `Failed to import skill: ${err}` }, 500);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// GET /api/skills/:id - Get a skill
|
|
117
|
+
const skillMatch = path.match(/^\/api\/skills\/([^/]+)$/);
|
|
118
|
+
|
|
119
|
+
// GET /api/skills/:id/export - Export a skill as SKILL.md
|
|
120
|
+
const skillExportMatch = path.match(/^\/api\/skills\/([^/]+)\/export$/);
|
|
121
|
+
if (skillExportMatch && method === "GET") {
|
|
122
|
+
const skill = SkillDB.findById(skillExportMatch[1]);
|
|
123
|
+
if (!skill) {
|
|
124
|
+
return json({ error: "Skill not found" }, 404);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Return the raw content
|
|
128
|
+
return new Response(skill.content, {
|
|
129
|
+
headers: {
|
|
130
|
+
"Content-Type": "text/markdown",
|
|
131
|
+
"Content-Disposition": `attachment; filename="${skill.name}-SKILL.md"`,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// POST /api/skills/:id/toggle - Toggle skill enabled/disabled
|
|
137
|
+
const skillToggleMatch = path.match(/^\/api\/skills\/([^/]+)\/toggle$/);
|
|
138
|
+
if (skillToggleMatch && method === "POST") {
|
|
139
|
+
const skill = SkillDB.findById(skillToggleMatch[1]);
|
|
140
|
+
if (!skill) {
|
|
141
|
+
return json({ error: "Skill not found" }, 404);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const updated = SkillDB.setEnabled(skillToggleMatch[1], !skill.enabled);
|
|
145
|
+
return json({ skill: updated });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ============ SkillsMP Marketplace ============
|
|
149
|
+
|
|
150
|
+
// GET /api/skills/marketplace/search - Search skills marketplace
|
|
151
|
+
if (path === "/api/skills/marketplace/search" && method === "GET") {
|
|
152
|
+
const url = new URL(req.url);
|
|
153
|
+
const query = url.searchParams.get("q") || "";
|
|
154
|
+
const page = parseInt(url.searchParams.get("page") || "1", 10);
|
|
155
|
+
|
|
156
|
+
// Get SkillsMP API key if configured
|
|
157
|
+
const skillsmpKey = ProviderKeys.getDecrypted("skillsmp");
|
|
158
|
+
|
|
159
|
+
const result = await SkillsmpProvider.search(skillsmpKey || "", query, page);
|
|
160
|
+
return json(result);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// GET /api/skills/marketplace/featured - Get featured skills
|
|
164
|
+
if (path === "/api/skills/marketplace/featured" && method === "GET") {
|
|
165
|
+
const skillsmpKey = ProviderKeys.getDecrypted("skillsmp");
|
|
166
|
+
const skills = await SkillsmpProvider.getFeatured(skillsmpKey || "");
|
|
167
|
+
return json({ skills });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// GET /api/skills/marketplace/:id - Get skill details from marketplace
|
|
171
|
+
const marketplaceSkillMatch = path.match(/^\/api\/skills\/marketplace\/([^/]+)$/);
|
|
172
|
+
if (marketplaceSkillMatch && method === "GET") {
|
|
173
|
+
const skillsmpKey = ProviderKeys.getDecrypted("skillsmp");
|
|
174
|
+
const skill = await SkillsmpProvider.getSkill(skillsmpKey || "", marketplaceSkillMatch[1]);
|
|
175
|
+
if (!skill) {
|
|
176
|
+
return json({ error: "Skill not found in marketplace" }, 404);
|
|
177
|
+
}
|
|
178
|
+
return json({ skill });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// POST /api/skills/marketplace/:id/install - Install a skill from marketplace
|
|
182
|
+
const marketplaceInstallMatch = path.match(/^\/api\/skills\/marketplace\/([^/]+)\/install$/);
|
|
183
|
+
if (marketplaceInstallMatch && method === "POST") {
|
|
184
|
+
const skillsmpKey = ProviderKeys.getDecrypted("skillsmp");
|
|
185
|
+
const marketplaceSkill = await SkillsmpProvider.getSkill(skillsmpKey || "", marketplaceInstallMatch[1]);
|
|
186
|
+
|
|
187
|
+
if (!marketplaceSkill) {
|
|
188
|
+
return json({ error: "Skill not found in marketplace" }, 404);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (SkillDB.exists(marketplaceSkill.name)) {
|
|
192
|
+
return json({ error: `A skill named "${marketplaceSkill.name}" already exists` }, 400);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const skill = SkillDB.create({
|
|
196
|
+
name: marketplaceSkill.name,
|
|
197
|
+
description: marketplaceSkill.description,
|
|
198
|
+
content: marketplaceSkill.content,
|
|
199
|
+
version: marketplaceSkill.version || "1.0.0",
|
|
200
|
+
license: marketplaceSkill.license,
|
|
201
|
+
compatibility: marketplaceSkill.compatibility,
|
|
202
|
+
metadata: {
|
|
203
|
+
author: marketplaceSkill.author,
|
|
204
|
+
version: marketplaceSkill.version,
|
|
205
|
+
...(marketplaceSkill.repository ? { repository: marketplaceSkill.repository } : {}),
|
|
206
|
+
},
|
|
207
|
+
allowed_tools: [],
|
|
208
|
+
source: "skillsmp",
|
|
209
|
+
project_id: null,
|
|
210
|
+
source_url: marketplaceSkill.repository || `https://skillsmp.com/skills/${marketplaceSkill.id}`,
|
|
211
|
+
enabled: true,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return json({ skill }, 201);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ============ GitHub Skills ============
|
|
218
|
+
|
|
219
|
+
// GET /api/skills/github/:owner/:repo - List skills from a GitHub repo
|
|
220
|
+
const githubRepoMatch = path.match(/^\/api\/skills\/github\/([^/]+)\/([^/]+)$/);
|
|
221
|
+
if (githubRepoMatch && method === "GET") {
|
|
222
|
+
const [, owner, repo] = githubRepoMatch;
|
|
223
|
+
|
|
224
|
+
const githubHeaders = {
|
|
225
|
+
"Accept": "application/vnd.github.v3+json",
|
|
226
|
+
"User-Agent": "Apteva-Skills-Browser",
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// Helper to fetch directory contents
|
|
230
|
+
const fetchDir = async (dirPath: string) => {
|
|
231
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/contents/${dirPath}`;
|
|
232
|
+
const res = await fetch(url, { headers: githubHeaders });
|
|
233
|
+
if (!res.ok) return [];
|
|
234
|
+
return await res.json() as Array<{
|
|
235
|
+
name: string;
|
|
236
|
+
path: string;
|
|
237
|
+
type: "file" | "dir";
|
|
238
|
+
size?: number;
|
|
239
|
+
download_url?: string;
|
|
240
|
+
}>;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// Helper to find skills in a directory (looks for subdirs with SKILL.md)
|
|
244
|
+
const findSkillsInDir = async (basePath: string) => {
|
|
245
|
+
const skills: Array<{
|
|
246
|
+
name: string;
|
|
247
|
+
description: string;
|
|
248
|
+
path: string;
|
|
249
|
+
size: number;
|
|
250
|
+
downloadUrl: string;
|
|
251
|
+
}> = [];
|
|
252
|
+
|
|
253
|
+
const contents = await fetchDir(basePath);
|
|
254
|
+
const skillDirs = contents.filter(item => item.type === "dir");
|
|
255
|
+
|
|
256
|
+
for (const dir of skillDirs) {
|
|
257
|
+
try {
|
|
258
|
+
const dirContents = await fetchDir(dir.path);
|
|
259
|
+
const skillFile = dirContents.find(
|
|
260
|
+
f => f.type === "file" && f.name.toLowerCase() === "skill.md"
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
if (skillFile && skillFile.download_url) {
|
|
264
|
+
const skillResponse = await fetch(skillFile.download_url);
|
|
265
|
+
if (skillResponse.ok) {
|
|
266
|
+
const content = await skillResponse.text();
|
|
267
|
+
|
|
268
|
+
// Parse frontmatter for description
|
|
269
|
+
let description = "";
|
|
270
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
271
|
+
if (frontmatterMatch) {
|
|
272
|
+
const descMatch = frontmatterMatch[1].match(/description:\s*["']?([^"'\n]+)["']?/);
|
|
273
|
+
if (descMatch) {
|
|
274
|
+
description = descMatch[1].trim();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// If no frontmatter description, try to get first paragraph
|
|
279
|
+
if (!description) {
|
|
280
|
+
const contentWithoutFrontmatter = content.replace(/^---\n[\s\S]*?\n---\n?/, "").trim();
|
|
281
|
+
const firstPara = contentWithoutFrontmatter.split("\n\n")[0];
|
|
282
|
+
if (firstPara && !firstPara.startsWith("#")) {
|
|
283
|
+
description = firstPara.slice(0, 200);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
skills.push({
|
|
288
|
+
name: dir.name,
|
|
289
|
+
description: description || `Skill from ${dir.name}`,
|
|
290
|
+
path: skillFile.path,
|
|
291
|
+
size: skillFile.size || 0,
|
|
292
|
+
downloadUrl: skillFile.download_url,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
} catch (e) {
|
|
297
|
+
// Skip this directory on error
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return skills;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
// Fetch root contents
|
|
306
|
+
const rootContents = await fetchDir("");
|
|
307
|
+
|
|
308
|
+
if (rootContents.length === 0) {
|
|
309
|
+
return json({ error: "Repository not found or empty" }, 404);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
let skills: Array<{
|
|
313
|
+
name: string;
|
|
314
|
+
description: string;
|
|
315
|
+
path: string;
|
|
316
|
+
size: number;
|
|
317
|
+
downloadUrl: string;
|
|
318
|
+
}> = [];
|
|
319
|
+
|
|
320
|
+
// Check common skill directory patterns: skills/, src/skills/, .claude/skills/
|
|
321
|
+
const skillsDirs = ["skills", "src/skills", ".claude/skills"];
|
|
322
|
+
for (const skillsDir of skillsDirs) {
|
|
323
|
+
const dirExists = rootContents.find(
|
|
324
|
+
item => item.type === "dir" && item.name === skillsDir.split("/")[0]
|
|
325
|
+
);
|
|
326
|
+
if (dirExists || skillsDir.includes("/")) {
|
|
327
|
+
const foundSkills = await findSkillsInDir(skillsDir);
|
|
328
|
+
if (foundSkills.length > 0) {
|
|
329
|
+
skills = foundSkills;
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// If no skills found in common dirs, check root level subdirectories
|
|
336
|
+
if (skills.length === 0) {
|
|
337
|
+
skills = await findSkillsInDir("");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Also check for SKILL.md in root (single-skill repo)
|
|
341
|
+
const rootSkillFile = rootContents.find(
|
|
342
|
+
f => f.type === "file" && f.name.toLowerCase() === "skill.md"
|
|
343
|
+
);
|
|
344
|
+
if (rootSkillFile && rootSkillFile.download_url) {
|
|
345
|
+
const skillResponse = await fetch(rootSkillFile.download_url);
|
|
346
|
+
if (skillResponse.ok) {
|
|
347
|
+
const content = await skillResponse.text();
|
|
348
|
+
let name = repo;
|
|
349
|
+
let description = "";
|
|
350
|
+
|
|
351
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
352
|
+
if (frontmatterMatch) {
|
|
353
|
+
const nameMatch = frontmatterMatch[1].match(/name:\s*["']?([^"'\n]+)["']?/);
|
|
354
|
+
const descMatch = frontmatterMatch[1].match(/description:\s*["']?([^"'\n]+)["']?/);
|
|
355
|
+
if (nameMatch) name = nameMatch[1].trim();
|
|
356
|
+
if (descMatch) description = descMatch[1].trim();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
skills.unshift({
|
|
360
|
+
name,
|
|
361
|
+
description: description || `Skill from ${repo}`,
|
|
362
|
+
path: rootSkillFile.path,
|
|
363
|
+
size: rootSkillFile.size || 0,
|
|
364
|
+
downloadUrl: rootSkillFile.download_url,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return json({
|
|
370
|
+
skills,
|
|
371
|
+
repo: { owner, repo, url: `https://github.com/${owner}/${repo}` }
|
|
372
|
+
});
|
|
373
|
+
} catch (e) {
|
|
374
|
+
console.error("GitHub API error:", e);
|
|
375
|
+
return json({ error: "Failed to fetch from GitHub" }, 500);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// POST /api/skills/github/install - Install a skill from GitHub
|
|
380
|
+
if (path === "/api/skills/github/install" && method === "POST") {
|
|
381
|
+
try {
|
|
382
|
+
const body = await req.json() as {
|
|
383
|
+
owner: string;
|
|
384
|
+
repo: string;
|
|
385
|
+
skillName: string;
|
|
386
|
+
downloadUrl: string;
|
|
387
|
+
projectId?: string | null;
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const { owner, repo, skillName, downloadUrl, projectId } = body;
|
|
391
|
+
|
|
392
|
+
if (!owner || !repo || !skillName || !downloadUrl) {
|
|
393
|
+
return json({ error: "owner, repo, skillName, and downloadUrl are required" }, 400);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Check if skill already exists
|
|
397
|
+
if (SkillDB.exists(skillName)) {
|
|
398
|
+
return json({ error: `A skill named "${skillName}" already exists` }, 400);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Fetch the skill content
|
|
402
|
+
const response = await fetch(downloadUrl);
|
|
403
|
+
if (!response.ok) {
|
|
404
|
+
return json({ error: "Failed to fetch skill content" }, 500);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const content = await response.text();
|
|
408
|
+
|
|
409
|
+
// Parse frontmatter
|
|
410
|
+
let name = skillName;
|
|
411
|
+
let description = "";
|
|
412
|
+
let version = "1.0.0";
|
|
413
|
+
let license = null;
|
|
414
|
+
let compatibility = null;
|
|
415
|
+
let skillContent = content;
|
|
416
|
+
|
|
417
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
418
|
+
if (frontmatterMatch) {
|
|
419
|
+
const frontmatter = frontmatterMatch[1];
|
|
420
|
+
skillContent = frontmatterMatch[2].trim();
|
|
421
|
+
|
|
422
|
+
const nameMatch = frontmatter.match(/name:\s*["']?([^"'\n]+)["']?/);
|
|
423
|
+
const descMatch = frontmatter.match(/description:\s*["']?([^"'\n]+)["']?/);
|
|
424
|
+
const versionMatch = frontmatter.match(/version:\s*["']?([^"'\n]+)["']?/);
|
|
425
|
+
const licenseMatch = frontmatter.match(/license:\s*["']?([^"'\n]+)["']?/);
|
|
426
|
+
const compatMatch = frontmatter.match(/compatibility:\s*["']?([^"'\n]+)["']?/);
|
|
427
|
+
|
|
428
|
+
if (nameMatch) name = nameMatch[1].trim();
|
|
429
|
+
if (descMatch) description = descMatch[1].trim();
|
|
430
|
+
if (versionMatch) version = versionMatch[1].trim();
|
|
431
|
+
if (licenseMatch) license = licenseMatch[1].trim();
|
|
432
|
+
if (compatMatch) compatibility = compatMatch[1].trim();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Create the skill in DB
|
|
436
|
+
const skill = SkillDB.create({
|
|
437
|
+
name,
|
|
438
|
+
description: description || `Skill from ${owner}/${repo}`,
|
|
439
|
+
content: skillContent,
|
|
440
|
+
version,
|
|
441
|
+
license,
|
|
442
|
+
compatibility,
|
|
443
|
+
metadata: { owner, repo, originalName: skillName },
|
|
444
|
+
allowed_tools: [],
|
|
445
|
+
source: "github",
|
|
446
|
+
source_url: `https://github.com/${owner}/${repo}/blob/main/${skillName}/SKILL.md`,
|
|
447
|
+
enabled: true,
|
|
448
|
+
project_id: projectId || null,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
return json({ skill }, 201);
|
|
452
|
+
} catch (e) {
|
|
453
|
+
console.error("GitHub install error:", e);
|
|
454
|
+
return json({ error: "Failed to install skill from GitHub" }, 500);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Skill CRUD by ID (must come after more specific routes like /toggle, /export, /marketplace, /github)
|
|
459
|
+
if (skillMatch && method === "GET") {
|
|
460
|
+
const skill = SkillDB.findById(skillMatch[1]);
|
|
461
|
+
if (!skill) {
|
|
462
|
+
return json({ error: "Skill not found" }, 404);
|
|
463
|
+
}
|
|
464
|
+
return json({ skill });
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// PUT /api/skills/:id - Update a skill
|
|
468
|
+
if (skillMatch && method === "PUT") {
|
|
469
|
+
const skill = SkillDB.findById(skillMatch[1]);
|
|
470
|
+
if (!skill) {
|
|
471
|
+
return json({ error: "Skill not found" }, 404);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
try {
|
|
475
|
+
const body = await req.json();
|
|
476
|
+
const updates: Partial<Skill> = {};
|
|
477
|
+
|
|
478
|
+
if (body.name !== undefined) updates.name = body.name;
|
|
479
|
+
if (body.description !== undefined) updates.description = body.description;
|
|
480
|
+
if (body.content !== undefined) updates.content = body.content;
|
|
481
|
+
if (body.license !== undefined) updates.license = body.license;
|
|
482
|
+
if (body.compatibility !== undefined) updates.compatibility = body.compatibility;
|
|
483
|
+
if (body.metadata !== undefined) updates.metadata = body.metadata;
|
|
484
|
+
if (body.allowed_tools !== undefined) updates.allowed_tools = body.allowed_tools;
|
|
485
|
+
if (body.enabled !== undefined) updates.enabled = body.enabled;
|
|
486
|
+
if (body.project_id !== undefined) updates.project_id = body.project_id;
|
|
487
|
+
|
|
488
|
+
// Auto-increment version if content changed
|
|
489
|
+
if (body.content !== undefined && body.content !== skill.content) {
|
|
490
|
+
const [major, minor, patch] = (skill.version || "1.0.0").split(".").map(Number);
|
|
491
|
+
updates.version = `${major}.${minor}.${patch + 1}`;
|
|
492
|
+
} else if (body.version !== undefined) {
|
|
493
|
+
updates.version = body.version;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const updated = SkillDB.update(skillMatch[1], updates);
|
|
497
|
+
|
|
498
|
+
// Push updated skill to all running agents that have it
|
|
499
|
+
const agentsWithSkill = AgentDB.findBySkill(skillMatch[1]);
|
|
500
|
+
const runningAgents = agentsWithSkill.filter(a => a.status === "running" && a.port);
|
|
501
|
+
|
|
502
|
+
for (const agent of runningAgents) {
|
|
503
|
+
try {
|
|
504
|
+
const providerKey = ProviderKeys.getDecrypted(agent.provider);
|
|
505
|
+
if (providerKey) {
|
|
506
|
+
const config = buildAgentConfig(agent, providerKey);
|
|
507
|
+
await pushConfigToAgent(agent.id, agent.port!, config);
|
|
508
|
+
// Push skills via /skills endpoint
|
|
509
|
+
if (config.skills?.definitions?.length > 0) {
|
|
510
|
+
await pushSkillsToAgent(agent.id, agent.port!, config.skills.definitions);
|
|
511
|
+
}
|
|
512
|
+
console.log(`Pushed skill update to agent ${agent.name}`);
|
|
513
|
+
}
|
|
514
|
+
} catch (err) {
|
|
515
|
+
console.error(`Failed to push skill update to agent ${agent.name}:`, err);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return json({ skill: updated, agents_updated: runningAgents.length });
|
|
520
|
+
} catch (err) {
|
|
521
|
+
console.error("Failed to update skill:", err);
|
|
522
|
+
return json({ error: `Failed to update skill: ${err}` }, 500);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// DELETE /api/skills/:id - Delete a skill
|
|
527
|
+
if (skillMatch && method === "DELETE") {
|
|
528
|
+
const skill = SkillDB.findById(skillMatch[1]);
|
|
529
|
+
if (!skill) {
|
|
530
|
+
return json({ error: "Skill not found" }, 404);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
SkillDB.delete(skillMatch[1]);
|
|
534
|
+
return json({ success: true });
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return null;
|
|
538
|
+
}
|