apteva 0.4.56 → 0.7.0
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/README.md +216 -54
- package/cli.js +35 -0
- package/install.js +92 -0
- package/package.json +12 -79
- package/LICENSE +0 -63
- package/bin/apteva.js +0 -196
- package/dist/ActivityPage.kxzzb4yc.js +0 -3
- package/dist/ApiDocsPage.zq998hbm.js +0 -4
- package/dist/App.55rea8mn.js +0 -61
- package/dist/App.5ywb23z4.js +0 -53
- package/dist/App.6thds120.js +0 -4
- package/dist/App.9tctxzqm.js +0 -8
- package/dist/App.a8r8ttaz.js +0 -4
- package/dist/App.agsv5bje.js +0 -4
- package/dist/App.cepapqmx.js +0 -4
- package/dist/App.dp041gb3.js +0 -221
- package/dist/App.fds72zb5.js +0 -4
- package/dist/App.fg9qj2dq.js +0 -4
- package/dist/App.ndfejbm9.js +0 -4
- package/dist/App.nxmfmq1h.js +0 -13
- package/dist/App.qdfyt8ba.js +0 -4
- package/dist/App.x2d0ygt6.js +0 -4
- package/dist/App.yt9p4nr3.js +0 -20
- package/dist/App.zn4mw16t.js +0 -1
- package/dist/ConnectionsPage.8r96ryw7.js +0 -3
- package/dist/McpPage.3cwh0gnd.js +0 -3
- package/dist/SettingsPage.ykgdh5ev.js +0 -3
- package/dist/SkillsPage.4np1s65b.js +0 -3
- package/dist/TasksPage.4g08t7p6.js +0 -3
- package/dist/TelemetryPage.72w9pwcp.js +0 -3
- package/dist/TestsPage.z4fk3r7r.js +0 -3
- package/dist/ThreadsPage.63tcajeh.js +0 -3
- package/dist/apteva-kit.css +0 -1
- package/dist/icon.png +0 -0
- package/dist/index.html +0 -16
- package/dist/styles.css +0 -1
- package/scripts/postinstall.mjs +0 -102
- package/src/auth/index.ts +0 -394
- package/src/auth/middleware.ts +0 -213
- package/src/binary.ts +0 -536
- package/src/channels/index.ts +0 -40
- package/src/channels/telegram.ts +0 -311
- package/src/crypto.ts +0 -301
- package/src/db-tests.ts +0 -174
- package/src/db.ts +0 -3133
- package/src/integrations/agentdojo.ts +0 -559
- package/src/integrations/composio.ts +0 -437
- package/src/integrations/index.ts +0 -87
- package/src/integrations/skillsmp.ts +0 -318
- package/src/mcp-client.ts +0 -605
- package/src/mcp-handler.ts +0 -394
- package/src/mcp-platform.ts +0 -2370
- package/src/openapi.ts +0 -2410
- package/src/providers.ts +0 -597
- package/src/routes/api/agent-utils.ts +0 -890
- package/src/routes/api/agents.ts +0 -916
- package/src/routes/api/api-keys.ts +0 -95
- package/src/routes/api/channels.ts +0 -182
- package/src/routes/api/helpers.ts +0 -12
- package/src/routes/api/integrations.ts +0 -639
- package/src/routes/api/mcp.ts +0 -574
- package/src/routes/api/meta-agent.ts +0 -195
- package/src/routes/api/projects.ts +0 -112
- package/src/routes/api/providers.ts +0 -424
- package/src/routes/api/skills.ts +0 -537
- package/src/routes/api/system.ts +0 -333
- package/src/routes/api/telemetry.ts +0 -203
- package/src/routes/api/tests.ts +0 -148
- package/src/routes/api/triggers.ts +0 -518
- package/src/routes/api/users.ts +0 -148
- package/src/routes/api/webhooks.ts +0 -171
- package/src/routes/api.ts +0 -53
- package/src/routes/auth.ts +0 -251
- package/src/routes/share.ts +0 -86
- package/src/routes/static.ts +0 -131
- package/src/server.ts +0 -642
- package/src/test-runner.ts +0 -598
- package/src/triggers/agentdojo.ts +0 -253
- package/src/triggers/composio.ts +0 -264
- package/src/triggers/index.ts +0 -71
- package/src/tui/AgentList.tsx +0 -145
- package/src/tui/App.tsx +0 -102
- package/src/tui/Login.tsx +0 -104
- package/src/tui/api.ts +0 -72
- package/src/tui/index.tsx +0 -7
- package/src/web/App.tsx +0 -455
- package/src/web/components/activity/ActivityPage.tsx +0 -314
- package/src/web/components/activity/index.ts +0 -1
- package/src/web/components/agents/AgentCard.tsx +0 -189
- package/src/web/components/agents/AgentPanel.tsx +0 -2244
- package/src/web/components/agents/AgentsView.tsx +0 -180
- package/src/web/components/agents/CreateAgentModal.tsx +0 -475
- package/src/web/components/agents/index.ts +0 -4
- package/src/web/components/api/ApiDocsPage.tsx +0 -842
- package/src/web/components/auth/CreateAccountStep.tsx +0 -176
- package/src/web/components/auth/LoginPage.tsx +0 -91
- package/src/web/components/auth/index.ts +0 -2
- package/src/web/components/common/Icons.tsx +0 -250
- package/src/web/components/common/LoadingSpinner.tsx +0 -44
- package/src/web/components/common/Modal.tsx +0 -199
- package/src/web/components/common/Select.tsx +0 -97
- package/src/web/components/common/index.ts +0 -20
- package/src/web/components/connections/ConnectionsPage.tsx +0 -54
- package/src/web/components/connections/IntegrationsTab.tsx +0 -170
- package/src/web/components/connections/OverviewTab.tsx +0 -137
- package/src/web/components/connections/TriggersTab.tsx +0 -1346
- package/src/web/components/dashboard/Dashboard.tsx +0 -572
- package/src/web/components/dashboard/index.ts +0 -1
- package/src/web/components/index.ts +0 -21
- package/src/web/components/layout/ErrorBanner.tsx +0 -18
- package/src/web/components/layout/Header.tsx +0 -332
- package/src/web/components/layout/Sidebar.tsx +0 -231
- package/src/web/components/layout/index.ts +0 -3
- package/src/web/components/mcp/IntegrationsPanel.tsx +0 -857
- package/src/web/components/mcp/McpPage.tsx +0 -2515
- package/src/web/components/mcp/index.ts +0 -1
- package/src/web/components/meta-agent/MetaAgent.tsx +0 -245
- package/src/web/components/onboarding/OnboardingWizard.tsx +0 -404
- package/src/web/components/onboarding/index.ts +0 -1
- package/src/web/components/settings/SettingsPage.tsx +0 -2776
- package/src/web/components/settings/index.ts +0 -1
- package/src/web/components/skills/SkillsPage.tsx +0 -1200
- package/src/web/components/tasks/TasksPage.tsx +0 -1116
- package/src/web/components/tasks/index.ts +0 -1
- package/src/web/components/telemetry/TelemetryPage.tsx +0 -1129
- package/src/web/components/tests/TestsPage.tsx +0 -594
- package/src/web/components/threads/ThreadsPage.tsx +0 -315
- package/src/web/context/AuthContext.tsx +0 -242
- package/src/web/context/ProjectContext.tsx +0 -214
- package/src/web/context/TelemetryContext.tsx +0 -299
- package/src/web/context/ThemeContext.tsx +0 -90
- package/src/web/context/UIModeContext.tsx +0 -49
- package/src/web/context/index.ts +0 -12
- package/src/web/hooks/index.ts +0 -3
- package/src/web/hooks/useAgents.ts +0 -115
- package/src/web/hooks/useOnboarding.ts +0 -20
- package/src/web/hooks/useProviders.ts +0 -75
- package/src/web/icon.png +0 -0
- package/src/web/index.html +0 -16
- package/src/web/styles.css +0 -118
- package/src/web/themes.ts +0 -162
- package/src/web/types.ts +0 -298
|
@@ -1,1200 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from "react";
|
|
2
|
-
import { useAuth, useProjects } from "../../context";
|
|
3
|
-
import { useConfirm, useAlert } from "../common/Modal";
|
|
4
|
-
import { Select } from "../common/Select";
|
|
5
|
-
|
|
6
|
-
interface Skill {
|
|
7
|
-
id: string;
|
|
8
|
-
name: string;
|
|
9
|
-
description: string;
|
|
10
|
-
content: string;
|
|
11
|
-
license: string | null;
|
|
12
|
-
compatibility: string | null;
|
|
13
|
-
metadata: Record<string, string>;
|
|
14
|
-
allowed_tools: string[];
|
|
15
|
-
source: "local" | "skillsmp" | "github" | "import";
|
|
16
|
-
source_url: string | null;
|
|
17
|
-
enabled: boolean;
|
|
18
|
-
project_id: string | null; // null = global
|
|
19
|
-
created_at: string;
|
|
20
|
-
updated_at: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface MarketplaceSkill {
|
|
24
|
-
id: string;
|
|
25
|
-
name: string;
|
|
26
|
-
description: string;
|
|
27
|
-
content: string;
|
|
28
|
-
author: string;
|
|
29
|
-
version: string;
|
|
30
|
-
license: string | null;
|
|
31
|
-
compatibility: string | null;
|
|
32
|
-
tags: string[];
|
|
33
|
-
downloads: number;
|
|
34
|
-
rating: number;
|
|
35
|
-
repository: string | null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
interface GitHubSkill {
|
|
39
|
-
name: string;
|
|
40
|
-
description: string;
|
|
41
|
-
path: string;
|
|
42
|
-
size: number;
|
|
43
|
-
downloadUrl: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function SkillsPage() {
|
|
47
|
-
const { authFetch } = useAuth();
|
|
48
|
-
const { projects, currentProjectId } = useProjects();
|
|
49
|
-
const [skills, setSkills] = useState<Skill[]>([]);
|
|
50
|
-
const [loading, setLoading] = useState(true);
|
|
51
|
-
const [activeTab, setActiveTab] = useState<"installed" | "marketplace" | "github">("installed");
|
|
52
|
-
const [showCreate, setShowCreate] = useState(false);
|
|
53
|
-
const [showImport, setShowImport] = useState(false);
|
|
54
|
-
const [selectedSkill, setSelectedSkill] = useState<Skill | null>(null);
|
|
55
|
-
const { confirm, ConfirmDialog } = useConfirm();
|
|
56
|
-
const { alert, AlertDialog } = useAlert();
|
|
57
|
-
|
|
58
|
-
const hasProjects = projects.length > 0;
|
|
59
|
-
|
|
60
|
-
// Marketplace state
|
|
61
|
-
const [searchQuery, setSearchQuery] = useState("");
|
|
62
|
-
const [marketplaceSkills, setMarketplaceSkills] = useState<MarketplaceSkill[]>([]);
|
|
63
|
-
const [marketplaceLoading, setMarketplaceLoading] = useState(false);
|
|
64
|
-
const [installing, setInstalling] = useState<string | null>(null);
|
|
65
|
-
|
|
66
|
-
// GitHub state
|
|
67
|
-
const [githubRepo, setGithubRepo] = useState("");
|
|
68
|
-
const [githubSkills, setGithubSkills] = useState<GitHubSkill[]>([]);
|
|
69
|
-
const [githubLoading, setGithubLoading] = useState(false);
|
|
70
|
-
const [githubError, setGithubError] = useState<string | null>(null);
|
|
71
|
-
const [githubRepoInfo, setGithubRepoInfo] = useState<{ owner: string; repo: string; url: string } | null>(null);
|
|
72
|
-
const [installingGithub, setInstallingGithub] = useState<string | null>(null);
|
|
73
|
-
const [githubProjectId, setGithubProjectId] = useState<string | null>(
|
|
74
|
-
currentProjectId && currentProjectId !== "unassigned" ? currentProjectId : null
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
// Filter skills based on global project selector
|
|
78
|
-
// When a project is selected, show global + that project's skills
|
|
79
|
-
const filteredSkills = skills.filter(skill => {
|
|
80
|
-
if (!currentProjectId) return true; // "All Projects" - show everything
|
|
81
|
-
if (currentProjectId === "unassigned") return skill.project_id === null; // Only global
|
|
82
|
-
// Project selected: show global + project-specific
|
|
83
|
-
return skill.project_id === null || skill.project_id === currentProjectId;
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
const fetchSkills = async () => {
|
|
87
|
-
try {
|
|
88
|
-
const res = await authFetch("/api/skills");
|
|
89
|
-
const data = await res.json();
|
|
90
|
-
setSkills(data.skills || []);
|
|
91
|
-
} catch (e) {
|
|
92
|
-
console.error("Failed to fetch skills:", e);
|
|
93
|
-
}
|
|
94
|
-
setLoading(false);
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
const searchMarketplace = async (query?: string) => {
|
|
98
|
-
setMarketplaceLoading(true);
|
|
99
|
-
try {
|
|
100
|
-
const q = query !== undefined ? query : searchQuery;
|
|
101
|
-
const endpoint = q
|
|
102
|
-
? `/api/skills/marketplace/search?q=${encodeURIComponent(q)}`
|
|
103
|
-
: "/api/skills/marketplace/featured";
|
|
104
|
-
const res = await authFetch(endpoint);
|
|
105
|
-
const data = await res.json();
|
|
106
|
-
setMarketplaceSkills(data.skills || []);
|
|
107
|
-
} catch (e) {
|
|
108
|
-
console.error("Failed to search marketplace:", e);
|
|
109
|
-
}
|
|
110
|
-
setMarketplaceLoading(false);
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
useEffect(() => {
|
|
114
|
-
fetchSkills();
|
|
115
|
-
}, [authFetch]);
|
|
116
|
-
|
|
117
|
-
useEffect(() => {
|
|
118
|
-
if (activeTab === "marketplace" && marketplaceSkills.length === 0) {
|
|
119
|
-
searchMarketplace("");
|
|
120
|
-
}
|
|
121
|
-
}, [activeTab]);
|
|
122
|
-
|
|
123
|
-
const toggleSkill = async (id: string) => {
|
|
124
|
-
try {
|
|
125
|
-
await authFetch(`/api/skills/${id}/toggle`, { method: "POST" });
|
|
126
|
-
fetchSkills();
|
|
127
|
-
} catch (e) {
|
|
128
|
-
console.error("Failed to toggle skill:", e);
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
const deleteSkill = async (id: string) => {
|
|
133
|
-
const confirmed = await confirm("Delete this skill?", { confirmText: "Delete", title: "Delete Skill" });
|
|
134
|
-
if (!confirmed) return;
|
|
135
|
-
try {
|
|
136
|
-
await authFetch(`/api/skills/${id}`, { method: "DELETE" });
|
|
137
|
-
if (selectedSkill?.id === id) {
|
|
138
|
-
setSelectedSkill(null);
|
|
139
|
-
}
|
|
140
|
-
fetchSkills();
|
|
141
|
-
} catch (e) {
|
|
142
|
-
console.error("Failed to delete skill:", e);
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
const installFromMarketplace = async (skill: MarketplaceSkill) => {
|
|
147
|
-
setInstalling(skill.id);
|
|
148
|
-
try {
|
|
149
|
-
const res = await authFetch(`/api/skills/marketplace/${skill.id}/install`, { method: "POST" });
|
|
150
|
-
const data = await res.json();
|
|
151
|
-
if (res.ok) {
|
|
152
|
-
await alert(`Installed "${skill.name}" successfully!`, { title: "Skill Installed" });
|
|
153
|
-
fetchSkills();
|
|
154
|
-
setActiveTab("installed");
|
|
155
|
-
} else {
|
|
156
|
-
await alert(data.error || "Failed to install skill", { title: "Installation Failed" });
|
|
157
|
-
}
|
|
158
|
-
} catch (e) {
|
|
159
|
-
console.error("Failed to install skill:", e);
|
|
160
|
-
await alert("Failed to install skill", { title: "Error" });
|
|
161
|
-
}
|
|
162
|
-
setInstalling(null);
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
const isInstalled = (name: string) => skills.some((s) => s.name === name);
|
|
166
|
-
|
|
167
|
-
// GitHub functions
|
|
168
|
-
const browseGitHubRepo = async (repoInput?: string) => {
|
|
169
|
-
const input = repoInput || githubRepo;
|
|
170
|
-
if (!input.trim()) return;
|
|
171
|
-
|
|
172
|
-
// Parse repo input: "owner/repo" or full URL
|
|
173
|
-
let owner = "";
|
|
174
|
-
let repo = "";
|
|
175
|
-
|
|
176
|
-
if (input.includes("github.com")) {
|
|
177
|
-
const match = input.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
178
|
-
if (match) {
|
|
179
|
-
owner = match[1];
|
|
180
|
-
repo = match[2].replace(/\.git$/, "");
|
|
181
|
-
}
|
|
182
|
-
} else if (input.includes("/")) {
|
|
183
|
-
const parts = input.split("/");
|
|
184
|
-
owner = parts[0];
|
|
185
|
-
repo = parts[1];
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (!owner || !repo) {
|
|
189
|
-
setGithubError("Invalid repo format. Use 'owner/repo' or GitHub URL");
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
setGithubLoading(true);
|
|
194
|
-
setGithubError(null);
|
|
195
|
-
setGithubSkills([]);
|
|
196
|
-
setGithubRepoInfo(null);
|
|
197
|
-
|
|
198
|
-
try {
|
|
199
|
-
const res = await authFetch(`/api/skills/github/${owner}/${repo}`);
|
|
200
|
-
const data = await res.json();
|
|
201
|
-
|
|
202
|
-
if (!res.ok) {
|
|
203
|
-
setGithubError(data.error || "Failed to fetch repository");
|
|
204
|
-
setGithubLoading(false);
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
setGithubSkills(data.skills || []);
|
|
209
|
-
setGithubRepoInfo(data.repo || null);
|
|
210
|
-
} catch (e) {
|
|
211
|
-
setGithubError("Failed to fetch repository");
|
|
212
|
-
}
|
|
213
|
-
setGithubLoading(false);
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
const installFromGitHub = async (skill: GitHubSkill) => {
|
|
217
|
-
if (!githubRepoInfo) return;
|
|
218
|
-
|
|
219
|
-
setInstallingGithub(skill.name);
|
|
220
|
-
try {
|
|
221
|
-
const res = await authFetch("/api/skills/github/install", {
|
|
222
|
-
method: "POST",
|
|
223
|
-
headers: { "Content-Type": "application/json" },
|
|
224
|
-
body: JSON.stringify({
|
|
225
|
-
owner: githubRepoInfo.owner,
|
|
226
|
-
repo: githubRepoInfo.repo,
|
|
227
|
-
skillName: skill.name,
|
|
228
|
-
downloadUrl: skill.downloadUrl,
|
|
229
|
-
projectId: githubProjectId,
|
|
230
|
-
}),
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
const data = await res.json();
|
|
234
|
-
if (res.ok) {
|
|
235
|
-
await alert(`Installed "${skill.name}" successfully!`, { title: "Skill Installed" });
|
|
236
|
-
fetchSkills();
|
|
237
|
-
} else {
|
|
238
|
-
await alert(data.error || "Failed to install skill", { title: "Installation Failed", variant: "error" });
|
|
239
|
-
}
|
|
240
|
-
} catch (e) {
|
|
241
|
-
await alert("Failed to install skill", { title: "Error", variant: "error" });
|
|
242
|
-
}
|
|
243
|
-
setInstallingGithub(null);
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
const installAllFromGitHub = async () => {
|
|
247
|
-
if (!githubRepoInfo || githubSkills.length === 0) return;
|
|
248
|
-
|
|
249
|
-
const uninstalled = githubSkills.filter(s => !isInstalled(s.name));
|
|
250
|
-
if (uninstalled.length === 0) {
|
|
251
|
-
await alert("All skills are already installed", { title: "Info" });
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const confirmed = await confirm(
|
|
256
|
-
`Install ${uninstalled.length} skill(s) from ${githubRepoInfo.owner}/${githubRepoInfo.repo}?`,
|
|
257
|
-
{ confirmText: "Install All", title: "Install Skills" }
|
|
258
|
-
);
|
|
259
|
-
if (!confirmed) return;
|
|
260
|
-
|
|
261
|
-
let installed = 0;
|
|
262
|
-
for (const skill of uninstalled) {
|
|
263
|
-
setInstallingGithub(skill.name);
|
|
264
|
-
try {
|
|
265
|
-
const res = await authFetch("/api/skills/github/install", {
|
|
266
|
-
method: "POST",
|
|
267
|
-
headers: { "Content-Type": "application/json" },
|
|
268
|
-
body: JSON.stringify({
|
|
269
|
-
owner: githubRepoInfo.owner,
|
|
270
|
-
repo: githubRepoInfo.repo,
|
|
271
|
-
skillName: skill.name,
|
|
272
|
-
downloadUrl: skill.downloadUrl,
|
|
273
|
-
projectId: githubProjectId,
|
|
274
|
-
}),
|
|
275
|
-
});
|
|
276
|
-
if (res.ok) installed++;
|
|
277
|
-
} catch (e) {
|
|
278
|
-
// Continue with others
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
setInstallingGithub(null);
|
|
282
|
-
fetchSkills();
|
|
283
|
-
await alert(`Installed ${installed} of ${uninstalled.length} skills`, { title: "Installation Complete" });
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
return (
|
|
287
|
-
<>
|
|
288
|
-
{ConfirmDialog}
|
|
289
|
-
{AlertDialog}
|
|
290
|
-
<div className="flex-1 overflow-auto p-6">
|
|
291
|
-
<div className="max-w-6xl">
|
|
292
|
-
{/* Header */}
|
|
293
|
-
<div className="flex items-center justify-between mb-6">
|
|
294
|
-
<div>
|
|
295
|
-
<h1 className="text-2xl font-semibold mb-1">Skills</h1>
|
|
296
|
-
<p className="text-[var(--color-text-muted)]">
|
|
297
|
-
Manage agent skills - instructions that teach agents how to perform tasks.
|
|
298
|
-
</p>
|
|
299
|
-
</div>
|
|
300
|
-
{activeTab === "installed" && (
|
|
301
|
-
<div className="flex gap-2">
|
|
302
|
-
<button
|
|
303
|
-
onClick={() => setShowImport(true)}
|
|
304
|
-
className="bg-[var(--color-surface-raised)] hover:bg-[var(--color-surface-raised)] text-white px-4 py-2 rounded font-medium transition border border-[var(--color-border-light)]"
|
|
305
|
-
>
|
|
306
|
-
Import
|
|
307
|
-
</button>
|
|
308
|
-
<button
|
|
309
|
-
onClick={() => setShowCreate(true)}
|
|
310
|
-
className="bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] text-black px-4 py-2 rounded font-medium transition"
|
|
311
|
-
>
|
|
312
|
-
+ Create Skill
|
|
313
|
-
</button>
|
|
314
|
-
</div>
|
|
315
|
-
)}
|
|
316
|
-
</div>
|
|
317
|
-
|
|
318
|
-
{/* Tabs */}
|
|
319
|
-
<div className="flex gap-1 mb-6 bg-[var(--color-surface)] card p-1 w-fit">
|
|
320
|
-
<button
|
|
321
|
-
onClick={() => setActiveTab("installed")}
|
|
322
|
-
className={`px-4 py-2 rounded text-sm font-medium transition ${
|
|
323
|
-
activeTab === "installed"
|
|
324
|
-
? "bg-[var(--color-surface-raised)] text-white"
|
|
325
|
-
: "text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
|
|
326
|
-
}`}
|
|
327
|
-
>
|
|
328
|
-
Installed ({filteredSkills.length})
|
|
329
|
-
</button>
|
|
330
|
-
<button
|
|
331
|
-
onClick={() => setActiveTab("github")}
|
|
332
|
-
className={`px-4 py-2 rounded text-sm font-medium transition ${
|
|
333
|
-
activeTab === "github"
|
|
334
|
-
? "bg-[var(--color-surface-raised)] text-white"
|
|
335
|
-
: "text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
|
|
336
|
-
}`}
|
|
337
|
-
>
|
|
338
|
-
Browse GitHub
|
|
339
|
-
</button>
|
|
340
|
-
<button
|
|
341
|
-
onClick={() => setActiveTab("marketplace")}
|
|
342
|
-
className={`px-4 py-2 rounded text-sm font-medium transition ${
|
|
343
|
-
activeTab === "marketplace"
|
|
344
|
-
? "bg-[var(--color-surface-raised)] text-white"
|
|
345
|
-
: "text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]"
|
|
346
|
-
}`}
|
|
347
|
-
>
|
|
348
|
-
Marketplace
|
|
349
|
-
</button>
|
|
350
|
-
</div>
|
|
351
|
-
|
|
352
|
-
{/* Installed Tab */}
|
|
353
|
-
{activeTab === "installed" && (
|
|
354
|
-
<>
|
|
355
|
-
{loading ? (
|
|
356
|
-
<div className="text-[var(--color-text-muted)]">Loading skills...</div>
|
|
357
|
-
) : skills.length === 0 ? (
|
|
358
|
-
<div className="text-center py-20 text-[var(--color-text-muted)]">
|
|
359
|
-
<p className="text-lg">No skills installed</p>
|
|
360
|
-
<p className="text-sm mt-1">Create a skill or browse the marketplace</p>
|
|
361
|
-
<button
|
|
362
|
-
onClick={() => setActiveTab("marketplace")}
|
|
363
|
-
className="mt-4 bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] text-black px-4 py-2 rounded font-medium transition"
|
|
364
|
-
>
|
|
365
|
-
Browse Marketplace
|
|
366
|
-
</button>
|
|
367
|
-
</div>
|
|
368
|
-
) : filteredSkills.length === 0 ? (
|
|
369
|
-
<div className="bg-[var(--color-surface)] card p-6 text-center">
|
|
370
|
-
<p className="text-[var(--color-text-muted)]">No skills match this filter.</p>
|
|
371
|
-
</div>
|
|
372
|
-
) : (
|
|
373
|
-
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
|
374
|
-
{filteredSkills.map((skill) => {
|
|
375
|
-
const project = hasProjects && skill.project_id
|
|
376
|
-
? projects.find(p => p.id === skill.project_id)
|
|
377
|
-
: null;
|
|
378
|
-
return (
|
|
379
|
-
<SkillCard
|
|
380
|
-
key={skill.id}
|
|
381
|
-
skill={skill}
|
|
382
|
-
project={project}
|
|
383
|
-
onToggle={() => toggleSkill(skill.id)}
|
|
384
|
-
onDelete={() => deleteSkill(skill.id)}
|
|
385
|
-
onView={() => setSelectedSkill(skill)}
|
|
386
|
-
/>
|
|
387
|
-
);
|
|
388
|
-
})}
|
|
389
|
-
</div>
|
|
390
|
-
)}
|
|
391
|
-
</>
|
|
392
|
-
)}
|
|
393
|
-
|
|
394
|
-
{/* GitHub Tab */}
|
|
395
|
-
{activeTab === "github" && (
|
|
396
|
-
<div className="space-y-6">
|
|
397
|
-
{/* Search */}
|
|
398
|
-
<form
|
|
399
|
-
onSubmit={(e) => {
|
|
400
|
-
e.preventDefault();
|
|
401
|
-
browseGitHubRepo();
|
|
402
|
-
}}
|
|
403
|
-
className="flex gap-2"
|
|
404
|
-
>
|
|
405
|
-
<input
|
|
406
|
-
type="text"
|
|
407
|
-
value={githubRepo}
|
|
408
|
-
onChange={(e) => setGithubRepo(e.target.value)}
|
|
409
|
-
placeholder="Enter GitHub repo (e.g., WordPress/agent-skills)"
|
|
410
|
-
className="flex-1 bg-[var(--color-surface)] border border-[var(--color-border-light)] rounded-lg px-4 py-3 focus:outline-none focus:border-[var(--color-accent)]"
|
|
411
|
-
/>
|
|
412
|
-
<button
|
|
413
|
-
type="submit"
|
|
414
|
-
disabled={githubLoading}
|
|
415
|
-
className="bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] disabled:opacity-50 text-black px-6 py-3 rounded-lg font-medium transition"
|
|
416
|
-
>
|
|
417
|
-
{githubLoading ? "..." : "Browse"}
|
|
418
|
-
</button>
|
|
419
|
-
</form>
|
|
420
|
-
|
|
421
|
-
{/* Project Scope Selector */}
|
|
422
|
-
{hasProjects && githubSkills.length > 0 && (
|
|
423
|
-
<div className="flex items-center gap-3 p-3 bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded-lg">
|
|
424
|
-
<span className="text-sm text-[var(--color-text-muted)]">Install to:</span>
|
|
425
|
-
<Select
|
|
426
|
-
value={githubProjectId || ""}
|
|
427
|
-
onChange={(value) => setGithubProjectId(value || null)}
|
|
428
|
-
options={[
|
|
429
|
-
{ value: "", label: "Global (all projects)" },
|
|
430
|
-
...projects.map(p => ({ value: p.id, label: p.name }))
|
|
431
|
-
]}
|
|
432
|
-
placeholder="Select scope..."
|
|
433
|
-
/>
|
|
434
|
-
</div>
|
|
435
|
-
)}
|
|
436
|
-
|
|
437
|
-
{/* Error */}
|
|
438
|
-
{githubError && (
|
|
439
|
-
<div className="text-red-400 text-sm p-3 bg-red-500/10 border border-red-500/20 rounded-lg">
|
|
440
|
-
{githubError}
|
|
441
|
-
</div>
|
|
442
|
-
)}
|
|
443
|
-
|
|
444
|
-
{/* Repo Info Header */}
|
|
445
|
-
{githubRepoInfo && githubSkills.length > 0 && (
|
|
446
|
-
<div className="flex items-center justify-between">
|
|
447
|
-
<div className="flex items-center gap-3">
|
|
448
|
-
<a
|
|
449
|
-
href={githubRepoInfo.url}
|
|
450
|
-
target="_blank"
|
|
451
|
-
rel="noopener noreferrer"
|
|
452
|
-
className="text-[var(--color-accent)] hover:underline font-medium"
|
|
453
|
-
>
|
|
454
|
-
{githubRepoInfo.owner}/{githubRepoInfo.repo}
|
|
455
|
-
</a>
|
|
456
|
-
<span className="text-sm text-[var(--color-text-muted)]">
|
|
457
|
-
{githubSkills.length} skill{githubSkills.length !== 1 ? "s" : ""} found
|
|
458
|
-
</span>
|
|
459
|
-
</div>
|
|
460
|
-
{githubSkills.some(s => !isInstalled(s.name)) && (
|
|
461
|
-
<button
|
|
462
|
-
onClick={installAllFromGitHub}
|
|
463
|
-
disabled={!!installingGithub}
|
|
464
|
-
className="text-sm bg-[var(--color-surface-raised)] hover:bg-[var(--color-surface-raised)] border border-[var(--color-border-light)] hover:border-[var(--color-accent)] px-4 py-2 rounded transition disabled:opacity-50"
|
|
465
|
-
>
|
|
466
|
-
Install All
|
|
467
|
-
</button>
|
|
468
|
-
)}
|
|
469
|
-
</div>
|
|
470
|
-
)}
|
|
471
|
-
|
|
472
|
-
{/* Loading */}
|
|
473
|
-
{githubLoading && (
|
|
474
|
-
<div className="text-center py-8 text-[var(--color-text-muted)]">
|
|
475
|
-
Fetching skills from repository...
|
|
476
|
-
</div>
|
|
477
|
-
)}
|
|
478
|
-
|
|
479
|
-
{/* Empty State */}
|
|
480
|
-
{!githubLoading && !githubRepoInfo && !githubError && (
|
|
481
|
-
<div className="bg-[var(--color-surface)] card p-8 text-center">
|
|
482
|
-
<div className="text-4xl mb-4">📦</div>
|
|
483
|
-
<h3 className="text-lg font-medium mb-2">Browse Skills from GitHub</h3>
|
|
484
|
-
<p className="text-[var(--color-text-muted)] mb-6 max-w-md mx-auto">
|
|
485
|
-
Enter a GitHub repository to browse and install skills. Skills are markdown files with instructions that teach agents how to perform specific tasks.
|
|
486
|
-
</p>
|
|
487
|
-
<div className="flex flex-wrap gap-2 justify-center">
|
|
488
|
-
{[
|
|
489
|
-
{ label: "WordPress Skills", repo: "WordPress/agent-skills" },
|
|
490
|
-
].map(({ label, repo }) => (
|
|
491
|
-
<button
|
|
492
|
-
key={repo}
|
|
493
|
-
onClick={() => {
|
|
494
|
-
setGithubRepo(repo);
|
|
495
|
-
browseGitHubRepo(repo);
|
|
496
|
-
}}
|
|
497
|
-
className="text-sm bg-[var(--color-surface-raised)] hover:bg-[var(--color-surface-raised)] border border-[var(--color-border-light)] hover:border-[var(--color-accent)] px-3 py-1.5 rounded transition"
|
|
498
|
-
>
|
|
499
|
-
{label}
|
|
500
|
-
</button>
|
|
501
|
-
))}
|
|
502
|
-
</div>
|
|
503
|
-
</div>
|
|
504
|
-
)}
|
|
505
|
-
|
|
506
|
-
{/* No Skills Found */}
|
|
507
|
-
{!githubLoading && githubRepoInfo && githubSkills.length === 0 && (
|
|
508
|
-
<div className="text-center py-8 text-[var(--color-text-muted)]">
|
|
509
|
-
No skills found in this repository. Skills should be in subdirectories with a SKILL.md file.
|
|
510
|
-
</div>
|
|
511
|
-
)}
|
|
512
|
-
|
|
513
|
-
{/* Skills Grid */}
|
|
514
|
-
{githubSkills.length > 0 && (
|
|
515
|
-
<div className="grid gap-4 md:grid-cols-2">
|
|
516
|
-
{githubSkills.map((skill) => {
|
|
517
|
-
const installed = isInstalled(skill.name);
|
|
518
|
-
const isInstalling = installingGithub === skill.name;
|
|
519
|
-
|
|
520
|
-
return (
|
|
521
|
-
<div
|
|
522
|
-
key={skill.name}
|
|
523
|
-
className={`bg-[var(--color-surface)] border rounded-lg p-4 transition ${
|
|
524
|
-
installed ? "border-green-500/30" : "border-[var(--color-border)] hover:border-[var(--color-border-light)]"
|
|
525
|
-
}`}
|
|
526
|
-
>
|
|
527
|
-
<div className="flex items-start justify-between gap-3">
|
|
528
|
-
<div className="flex-1 min-w-0">
|
|
529
|
-
<div className="flex items-center gap-2">
|
|
530
|
-
<h3 className="font-medium truncate">{skill.name}</h3>
|
|
531
|
-
{installed && (
|
|
532
|
-
<span className="text-xs text-green-400">✓ Installed</span>
|
|
533
|
-
)}
|
|
534
|
-
</div>
|
|
535
|
-
<p className="text-sm text-[var(--color-text-muted)] mt-1 line-clamp-2">
|
|
536
|
-
{skill.description || "No description"}
|
|
537
|
-
</p>
|
|
538
|
-
<div className="flex items-center gap-2 mt-2 text-xs text-[var(--color-text-faint)]">
|
|
539
|
-
<span>{(skill.size / 1024).toFixed(1)}KB</span>
|
|
540
|
-
<span className="px-1.5 py-0.5 rounded bg-blue-500/10 text-blue-400">
|
|
541
|
-
GitHub
|
|
542
|
-
</span>
|
|
543
|
-
</div>
|
|
544
|
-
</div>
|
|
545
|
-
<div className="flex-shrink-0">
|
|
546
|
-
{installed ? (
|
|
547
|
-
<span className="text-xs text-[var(--color-text-faint)] px-3 py-1.5">Added</span>
|
|
548
|
-
) : (
|
|
549
|
-
<button
|
|
550
|
-
onClick={() => installFromGitHub(skill)}
|
|
551
|
-
disabled={isInstalling}
|
|
552
|
-
className="text-sm bg-[var(--color-surface-raised)] hover:bg-[var(--color-surface-raised)] border border-[var(--color-border-light)] hover:border-[var(--color-accent)] px-3 py-1.5 rounded transition disabled:opacity-50"
|
|
553
|
-
>
|
|
554
|
-
{isInstalling ? "Installing..." : "Install"}
|
|
555
|
-
</button>
|
|
556
|
-
)}
|
|
557
|
-
</div>
|
|
558
|
-
</div>
|
|
559
|
-
</div>
|
|
560
|
-
);
|
|
561
|
-
})}
|
|
562
|
-
</div>
|
|
563
|
-
)}
|
|
564
|
-
|
|
565
|
-
{/* Info */}
|
|
566
|
-
<div className="p-4 bg-[var(--color-surface)] card text-sm text-[var(--color-text-muted)]">
|
|
567
|
-
<p>
|
|
568
|
-
Skills are sourced from GitHub repositories. Each skill should be in its own directory with a{" "}
|
|
569
|
-
<code className="text-[var(--color-text-secondary)] bg-[var(--color-bg)] px-1 rounded">SKILL.md</code> file containing instructions.
|
|
570
|
-
</p>
|
|
571
|
-
</div>
|
|
572
|
-
</div>
|
|
573
|
-
)}
|
|
574
|
-
|
|
575
|
-
{/* Marketplace Tab */}
|
|
576
|
-
{activeTab === "marketplace" && (
|
|
577
|
-
<>
|
|
578
|
-
{/* Search */}
|
|
579
|
-
<div className="mb-6">
|
|
580
|
-
<div className="flex gap-2">
|
|
581
|
-
<input
|
|
582
|
-
type="text"
|
|
583
|
-
value={searchQuery}
|
|
584
|
-
onChange={(e) => setSearchQuery(e.target.value)}
|
|
585
|
-
onKeyDown={(e) => e.key === "Enter" && searchMarketplace()}
|
|
586
|
-
placeholder="Search skills..."
|
|
587
|
-
className="flex-1 bg-[var(--color-surface)] border border-[var(--color-border)] rounded px-4 py-2 focus:outline-none focus:border-[var(--color-accent)]"
|
|
588
|
-
/>
|
|
589
|
-
<button
|
|
590
|
-
onClick={() => searchMarketplace()}
|
|
591
|
-
disabled={marketplaceLoading}
|
|
592
|
-
className="bg-[var(--color-surface-raised)] hover:bg-[var(--color-surface-raised)] text-white px-4 py-2 rounded font-medium transition border border-[var(--color-border-light)]"
|
|
593
|
-
>
|
|
594
|
-
{marketplaceLoading ? "..." : "Search"}
|
|
595
|
-
</button>
|
|
596
|
-
</div>
|
|
597
|
-
</div>
|
|
598
|
-
|
|
599
|
-
{marketplaceLoading ? (
|
|
600
|
-
<div className="text-[var(--color-text-muted)]">Loading...</div>
|
|
601
|
-
) : marketplaceSkills.length === 0 ? (
|
|
602
|
-
<div className="text-center py-20 text-[var(--color-text-muted)]">
|
|
603
|
-
<p className="text-lg">No skills found</p>
|
|
604
|
-
<p className="text-sm mt-1">Try a different search term</p>
|
|
605
|
-
</div>
|
|
606
|
-
) : (
|
|
607
|
-
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
|
608
|
-
{marketplaceSkills.map((skill) => (
|
|
609
|
-
<MarketplaceSkillCard
|
|
610
|
-
key={skill.id}
|
|
611
|
-
skill={skill}
|
|
612
|
-
installed={isInstalled(skill.name)}
|
|
613
|
-
installing={installing === skill.id}
|
|
614
|
-
onInstall={() => installFromMarketplace(skill)}
|
|
615
|
-
/>
|
|
616
|
-
))}
|
|
617
|
-
</div>
|
|
618
|
-
)}
|
|
619
|
-
</>
|
|
620
|
-
)}
|
|
621
|
-
</div>
|
|
622
|
-
</div>
|
|
623
|
-
|
|
624
|
-
{/* Create Modal */}
|
|
625
|
-
{showCreate && (
|
|
626
|
-
<CreateSkillModal
|
|
627
|
-
authFetch={authFetch}
|
|
628
|
-
onClose={() => setShowCreate(false)}
|
|
629
|
-
onCreated={() => {
|
|
630
|
-
setShowCreate(false);
|
|
631
|
-
fetchSkills();
|
|
632
|
-
}}
|
|
633
|
-
projects={hasProjects ? projects : undefined}
|
|
634
|
-
defaultProjectId={currentProjectId && currentProjectId !== "unassigned" ? currentProjectId : null}
|
|
635
|
-
/>
|
|
636
|
-
)}
|
|
637
|
-
|
|
638
|
-
{/* Import Modal */}
|
|
639
|
-
{showImport && (
|
|
640
|
-
<ImportSkillModal
|
|
641
|
-
authFetch={authFetch}
|
|
642
|
-
onClose={() => setShowImport(false)}
|
|
643
|
-
onImported={() => {
|
|
644
|
-
setShowImport(false);
|
|
645
|
-
fetchSkills();
|
|
646
|
-
}}
|
|
647
|
-
/>
|
|
648
|
-
)}
|
|
649
|
-
|
|
650
|
-
{/* View/Edit Modal */}
|
|
651
|
-
{selectedSkill && (
|
|
652
|
-
<ViewSkillModal
|
|
653
|
-
skill={selectedSkill}
|
|
654
|
-
authFetch={authFetch}
|
|
655
|
-
onClose={() => setSelectedSkill(null)}
|
|
656
|
-
onUpdated={() => {
|
|
657
|
-
setSelectedSkill(null);
|
|
658
|
-
fetchSkills();
|
|
659
|
-
}}
|
|
660
|
-
/>
|
|
661
|
-
)}
|
|
662
|
-
</>
|
|
663
|
-
);
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
function SkillCard({
|
|
667
|
-
skill,
|
|
668
|
-
project,
|
|
669
|
-
onToggle,
|
|
670
|
-
onDelete,
|
|
671
|
-
onView,
|
|
672
|
-
}: {
|
|
673
|
-
skill: Skill;
|
|
674
|
-
project?: { id: string; name: string; color: string } | null;
|
|
675
|
-
onToggle: () => void;
|
|
676
|
-
onDelete: () => void;
|
|
677
|
-
onView: () => void;
|
|
678
|
-
}) {
|
|
679
|
-
const sourceLabel = {
|
|
680
|
-
local: "Local",
|
|
681
|
-
skillsmp: "SkillsMP",
|
|
682
|
-
github: "GitHub",
|
|
683
|
-
import: "Imported",
|
|
684
|
-
}[skill.source];
|
|
685
|
-
|
|
686
|
-
// Scope badge: Global or Project name
|
|
687
|
-
const getScopeBadge = () => {
|
|
688
|
-
if (project) {
|
|
689
|
-
return (
|
|
690
|
-
<span
|
|
691
|
-
className="text-xs px-1.5 py-0.5 rounded"
|
|
692
|
-
style={{ backgroundColor: `${project.color}20`, color: project.color }}
|
|
693
|
-
>
|
|
694
|
-
{project.name}
|
|
695
|
-
</span>
|
|
696
|
-
);
|
|
697
|
-
}
|
|
698
|
-
if (skill.project_id === null) {
|
|
699
|
-
return (
|
|
700
|
-
<span className="text-xs text-[var(--color-text-muted)] bg-[var(--color-surface-raised)] px-1.5 py-0.5 rounded">
|
|
701
|
-
Global
|
|
702
|
-
</span>
|
|
703
|
-
);
|
|
704
|
-
}
|
|
705
|
-
return null;
|
|
706
|
-
};
|
|
707
|
-
|
|
708
|
-
return (
|
|
709
|
-
<div
|
|
710
|
-
className={`bg-[var(--color-surface)] rounded-lg p-5 border transition cursor-pointer ${
|
|
711
|
-
skill.enabled ? "border-[var(--color-border)]" : "border-[var(--color-border)] opacity-60"
|
|
712
|
-
} hover:border-[var(--color-border-light)]`}
|
|
713
|
-
onClick={onView}
|
|
714
|
-
>
|
|
715
|
-
<div className="flex items-start justify-between mb-3">
|
|
716
|
-
<div className="flex-1 min-w-0">
|
|
717
|
-
<div className="flex items-center gap-2">
|
|
718
|
-
<h3 className="font-semibold text-lg truncate">{skill.name}</h3>
|
|
719
|
-
{getScopeBadge()}
|
|
720
|
-
</div>
|
|
721
|
-
<p className="text-xs text-[var(--color-text-muted)] flex items-center gap-2 mt-0.5">
|
|
722
|
-
<span className={`px-1.5 py-0.5 rounded text-[10px] ${
|
|
723
|
-
skill.source === "skillsmp" ? "bg-purple-500/20 text-purple-400" :
|
|
724
|
-
skill.source === "github" ? "bg-blue-500/20 text-blue-400" :
|
|
725
|
-
"bg-[var(--color-surface-raised)] text-[var(--color-text-secondary)]"
|
|
726
|
-
}`}>
|
|
727
|
-
{sourceLabel}
|
|
728
|
-
</span>
|
|
729
|
-
{skill.metadata?.version && <span>v{skill.metadata.version}</span>}
|
|
730
|
-
</p>
|
|
731
|
-
</div>
|
|
732
|
-
<button
|
|
733
|
-
onClick={(e) => {
|
|
734
|
-
e.stopPropagation();
|
|
735
|
-
onToggle();
|
|
736
|
-
}}
|
|
737
|
-
className={`w-10 h-5 rounded-full transition-colors relative ${
|
|
738
|
-
skill.enabled ? "bg-[var(--color-accent)]" : "bg-[var(--color-surface-raised)]"
|
|
739
|
-
}`}
|
|
740
|
-
>
|
|
741
|
-
<span
|
|
742
|
-
className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-transform ${
|
|
743
|
-
skill.enabled ? "left-5" : "left-0.5"
|
|
744
|
-
}`}
|
|
745
|
-
/>
|
|
746
|
-
</button>
|
|
747
|
-
</div>
|
|
748
|
-
|
|
749
|
-
<p className="text-sm text-[var(--color-text-secondary)] line-clamp-2 mb-4">{skill.description}</p>
|
|
750
|
-
|
|
751
|
-
<div className="flex items-center justify-between">
|
|
752
|
-
<div className="flex gap-1 flex-wrap">
|
|
753
|
-
{skill.allowed_tools.slice(0, 2).map((tool) => (
|
|
754
|
-
<span key={tool} className="text-xs bg-[var(--color-surface-raised)] px-2 py-0.5 rounded text-[var(--color-text-muted)]">
|
|
755
|
-
{tool}
|
|
756
|
-
</span>
|
|
757
|
-
))}
|
|
758
|
-
{skill.allowed_tools.length > 2 && (
|
|
759
|
-
<span className="text-xs text-[var(--color-text-muted)]">+{skill.allowed_tools.length - 2}</span>
|
|
760
|
-
)}
|
|
761
|
-
</div>
|
|
762
|
-
<button
|
|
763
|
-
onClick={(e) => {
|
|
764
|
-
e.stopPropagation();
|
|
765
|
-
onDelete();
|
|
766
|
-
}}
|
|
767
|
-
className="text-red-400 hover:text-red-300 text-sm"
|
|
768
|
-
>
|
|
769
|
-
Delete
|
|
770
|
-
</button>
|
|
771
|
-
</div>
|
|
772
|
-
</div>
|
|
773
|
-
);
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
function MarketplaceSkillCard({
|
|
777
|
-
skill,
|
|
778
|
-
installed,
|
|
779
|
-
installing,
|
|
780
|
-
onInstall,
|
|
781
|
-
}: {
|
|
782
|
-
skill: MarketplaceSkill;
|
|
783
|
-
installed: boolean;
|
|
784
|
-
installing: boolean;
|
|
785
|
-
onInstall: () => void;
|
|
786
|
-
}) {
|
|
787
|
-
return (
|
|
788
|
-
<div className="bg-[var(--color-surface)] rounded-lg p-5 border border-[var(--color-border)] hover:border-[var(--color-border-light)] transition">
|
|
789
|
-
<div className="flex items-start justify-between mb-3">
|
|
790
|
-
<div className="flex-1 min-w-0">
|
|
791
|
-
<h3 className="font-semibold text-lg truncate">{skill.name}</h3>
|
|
792
|
-
<p className="text-xs text-[var(--color-text-muted)] mt-0.5">
|
|
793
|
-
by {skill.author} · v{skill.version}
|
|
794
|
-
</p>
|
|
795
|
-
</div>
|
|
796
|
-
<div className="flex items-center gap-1 text-yellow-500 text-sm">
|
|
797
|
-
★ {skill.rating.toFixed(1)}
|
|
798
|
-
</div>
|
|
799
|
-
</div>
|
|
800
|
-
|
|
801
|
-
<p className="text-sm text-[var(--color-text-secondary)] line-clamp-2 mb-4">{skill.description}</p>
|
|
802
|
-
|
|
803
|
-
<div className="flex items-center justify-between">
|
|
804
|
-
<div className="flex gap-1 flex-wrap">
|
|
805
|
-
{skill.tags.slice(0, 3).map((tag) => (
|
|
806
|
-
<span key={tag} className="text-xs bg-[var(--color-surface-raised)] px-2 py-0.5 rounded text-[var(--color-text-muted)]">
|
|
807
|
-
{tag}
|
|
808
|
-
</span>
|
|
809
|
-
))}
|
|
810
|
-
</div>
|
|
811
|
-
{installed ? (
|
|
812
|
-
<span className="text-green-400 text-sm">✓ Installed</span>
|
|
813
|
-
) : (
|
|
814
|
-
<button
|
|
815
|
-
onClick={onInstall}
|
|
816
|
-
disabled={installing}
|
|
817
|
-
className="bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] disabled:opacity-50 text-black px-3 py-1 rounded text-sm font-medium transition"
|
|
818
|
-
>
|
|
819
|
-
{installing ? "Installing..." : "Install"}
|
|
820
|
-
</button>
|
|
821
|
-
)}
|
|
822
|
-
</div>
|
|
823
|
-
|
|
824
|
-
<div className="mt-3 text-xs text-[var(--color-text-faint)]">
|
|
825
|
-
{skill.downloads.toLocaleString()} downloads
|
|
826
|
-
</div>
|
|
827
|
-
</div>
|
|
828
|
-
);
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
function CreateSkillModal({
|
|
832
|
-
authFetch,
|
|
833
|
-
onClose,
|
|
834
|
-
onCreated,
|
|
835
|
-
projects,
|
|
836
|
-
defaultProjectId,
|
|
837
|
-
}: {
|
|
838
|
-
authFetch: (url: string, options?: RequestInit) => Promise<Response>;
|
|
839
|
-
onClose: () => void;
|
|
840
|
-
onCreated: () => void;
|
|
841
|
-
projects?: Array<{ id: string; name: string; color: string }>;
|
|
842
|
-
defaultProjectId?: string | null;
|
|
843
|
-
}) {
|
|
844
|
-
const [name, setName] = useState("");
|
|
845
|
-
const [description, setDescription] = useState("");
|
|
846
|
-
const [content, setContent] = useState("");
|
|
847
|
-
const [projectId, setProjectId] = useState<string | null>(defaultProjectId || null);
|
|
848
|
-
const [saving, setSaving] = useState(false);
|
|
849
|
-
const [error, setError] = useState<string | null>(null);
|
|
850
|
-
|
|
851
|
-
const hasProjects = projects && projects.length > 0;
|
|
852
|
-
|
|
853
|
-
const handleSave = async () => {
|
|
854
|
-
if (!name || !description || !content) {
|
|
855
|
-
setError("All fields are required");
|
|
856
|
-
return;
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
setSaving(true);
|
|
860
|
-
setError(null);
|
|
861
|
-
|
|
862
|
-
try {
|
|
863
|
-
const body: Record<string, unknown> = {
|
|
864
|
-
name,
|
|
865
|
-
description,
|
|
866
|
-
content, // Just the instructions, not wrapped in frontmatter
|
|
867
|
-
source: "local",
|
|
868
|
-
};
|
|
869
|
-
|
|
870
|
-
// Add project_id if selected
|
|
871
|
-
if (projectId) {
|
|
872
|
-
body.project_id = projectId;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
const res = await authFetch("/api/skills", {
|
|
876
|
-
method: "POST",
|
|
877
|
-
headers: { "Content-Type": "application/json" },
|
|
878
|
-
body: JSON.stringify(body),
|
|
879
|
-
});
|
|
880
|
-
|
|
881
|
-
const data = await res.json();
|
|
882
|
-
if (!res.ok) {
|
|
883
|
-
setError(data.error || "Failed to create skill");
|
|
884
|
-
setSaving(false);
|
|
885
|
-
return;
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
onCreated();
|
|
889
|
-
} catch (e) {
|
|
890
|
-
setError("Failed to create skill");
|
|
891
|
-
setSaving(false);
|
|
892
|
-
}
|
|
893
|
-
};
|
|
894
|
-
|
|
895
|
-
return (
|
|
896
|
-
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4" onClick={onClose}>
|
|
897
|
-
<div
|
|
898
|
-
className="bg-[var(--color-surface)] card w-full max-w-2xl max-h-[90vh] overflow-auto"
|
|
899
|
-
onClick={(e) => e.stopPropagation()}
|
|
900
|
-
>
|
|
901
|
-
<div className="p-6 border-b border-[var(--color-border)]">
|
|
902
|
-
<h2 className="text-xl font-semibold">Create Skill</h2>
|
|
903
|
-
</div>
|
|
904
|
-
|
|
905
|
-
<div className="p-6 space-y-4">
|
|
906
|
-
{error && (
|
|
907
|
-
<div className="bg-red-500/10 border border-red-500/30 rounded p-3 text-red-400 text-sm">
|
|
908
|
-
{error}
|
|
909
|
-
</div>
|
|
910
|
-
)}
|
|
911
|
-
|
|
912
|
-
<div>
|
|
913
|
-
<label className="block text-sm text-[var(--color-text-secondary)] mb-1">Name</label>
|
|
914
|
-
<input
|
|
915
|
-
type="text"
|
|
916
|
-
value={name}
|
|
917
|
-
onChange={(e) => setName(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, "-"))}
|
|
918
|
-
placeholder="my-skill-name"
|
|
919
|
-
className="w-full bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-3 py-2 focus:outline-none focus:border-[var(--color-accent)]"
|
|
920
|
-
/>
|
|
921
|
-
<p className="text-xs text-[var(--color-text-faint)] mt-1">Lowercase letters, numbers, and hyphens only</p>
|
|
922
|
-
</div>
|
|
923
|
-
|
|
924
|
-
<div>
|
|
925
|
-
<label className="block text-sm text-[var(--color-text-secondary)] mb-1">Description</label>
|
|
926
|
-
<input
|
|
927
|
-
type="text"
|
|
928
|
-
value={description}
|
|
929
|
-
onChange={(e) => setDescription(e.target.value)}
|
|
930
|
-
placeholder="What this skill does and when to use it..."
|
|
931
|
-
className="w-full bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-3 py-2 focus:outline-none focus:border-[var(--color-accent)]"
|
|
932
|
-
/>
|
|
933
|
-
</div>
|
|
934
|
-
|
|
935
|
-
{/* Project Scope - only show when projects exist */}
|
|
936
|
-
{hasProjects && (
|
|
937
|
-
<div>
|
|
938
|
-
<label className="block text-sm text-[var(--color-text-secondary)] mb-1">Scope</label>
|
|
939
|
-
<Select
|
|
940
|
-
value={projectId || ""}
|
|
941
|
-
onChange={(value) => setProjectId(value || null)}
|
|
942
|
-
options={[
|
|
943
|
-
{ value: "", label: "Global (all projects)" },
|
|
944
|
-
...projects!.map(p => ({ value: p.id, label: p.name }))
|
|
945
|
-
]}
|
|
946
|
-
placeholder="Select scope..."
|
|
947
|
-
/>
|
|
948
|
-
<p className="text-xs text-[var(--color-text-faint)] mt-1">
|
|
949
|
-
Global skills are available to all agents. Project-scoped skills are only available to agents in that project.
|
|
950
|
-
</p>
|
|
951
|
-
</div>
|
|
952
|
-
)}
|
|
953
|
-
|
|
954
|
-
<div>
|
|
955
|
-
<label className="block text-sm text-[var(--color-text-secondary)] mb-1">Instructions (Markdown)</label>
|
|
956
|
-
<textarea
|
|
957
|
-
value={content}
|
|
958
|
-
onChange={(e) => setContent(e.target.value)}
|
|
959
|
-
placeholder="# Skill Instructions Write detailed instructions here..."
|
|
960
|
-
rows={12}
|
|
961
|
-
className="w-full bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-3 py-2 focus:outline-none focus:border-[var(--color-accent)] font-mono text-sm"
|
|
962
|
-
/>
|
|
963
|
-
</div>
|
|
964
|
-
</div>
|
|
965
|
-
|
|
966
|
-
<div className="p-6 border-t border-[var(--color-border)] flex justify-end gap-3">
|
|
967
|
-
<button
|
|
968
|
-
onClick={onClose}
|
|
969
|
-
className="px-4 py-2 text-[var(--color-text-secondary)] hover:text-white transition"
|
|
970
|
-
>
|
|
971
|
-
Cancel
|
|
972
|
-
</button>
|
|
973
|
-
<button
|
|
974
|
-
onClick={handleSave}
|
|
975
|
-
disabled={saving}
|
|
976
|
-
className="bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] disabled:opacity-50 text-black px-4 py-2 rounded font-medium transition"
|
|
977
|
-
>
|
|
978
|
-
{saving ? "Creating..." : "Create Skill"}
|
|
979
|
-
</button>
|
|
980
|
-
</div>
|
|
981
|
-
</div>
|
|
982
|
-
</div>
|
|
983
|
-
);
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
function ImportSkillModal({
|
|
987
|
-
authFetch,
|
|
988
|
-
onClose,
|
|
989
|
-
onImported,
|
|
990
|
-
}: {
|
|
991
|
-
authFetch: (url: string, options?: RequestInit) => Promise<Response>;
|
|
992
|
-
onClose: () => void;
|
|
993
|
-
onImported: () => void;
|
|
994
|
-
}) {
|
|
995
|
-
const [content, setContent] = useState("");
|
|
996
|
-
const [importing, setImporting] = useState(false);
|
|
997
|
-
const [error, setError] = useState<string | null>(null);
|
|
998
|
-
|
|
999
|
-
const handleImport = async () => {
|
|
1000
|
-
if (!content.trim()) {
|
|
1001
|
-
setError("Paste SKILL.md content");
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
setImporting(true);
|
|
1006
|
-
setError(null);
|
|
1007
|
-
|
|
1008
|
-
try {
|
|
1009
|
-
const res = await authFetch("/api/skills/import", {
|
|
1010
|
-
method: "POST",
|
|
1011
|
-
headers: { "Content-Type": "application/json" },
|
|
1012
|
-
body: JSON.stringify({ content }),
|
|
1013
|
-
});
|
|
1014
|
-
|
|
1015
|
-
const data = await res.json();
|
|
1016
|
-
if (!res.ok) {
|
|
1017
|
-
setError(data.error || "Failed to import skill");
|
|
1018
|
-
setImporting(false);
|
|
1019
|
-
return;
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
onImported();
|
|
1023
|
-
} catch (e) {
|
|
1024
|
-
setError("Failed to import skill");
|
|
1025
|
-
setImporting(false);
|
|
1026
|
-
}
|
|
1027
|
-
};
|
|
1028
|
-
|
|
1029
|
-
return (
|
|
1030
|
-
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4" onClick={onClose}>
|
|
1031
|
-
<div
|
|
1032
|
-
className="bg-[var(--color-surface)] card w-full max-w-2xl max-h-[90vh] overflow-auto"
|
|
1033
|
-
onClick={(e) => e.stopPropagation()}
|
|
1034
|
-
>
|
|
1035
|
-
<div className="p-6 border-b border-[var(--color-border)]">
|
|
1036
|
-
<h2 className="text-xl font-semibold">Import Skill</h2>
|
|
1037
|
-
<p className="text-sm text-[var(--color-text-muted)] mt-1">Paste the contents of a SKILL.md file</p>
|
|
1038
|
-
</div>
|
|
1039
|
-
|
|
1040
|
-
<div className="p-6 space-y-4">
|
|
1041
|
-
{error && (
|
|
1042
|
-
<div className="bg-red-500/10 border border-red-500/30 rounded p-3 text-red-400 text-sm">
|
|
1043
|
-
{error}
|
|
1044
|
-
</div>
|
|
1045
|
-
)}
|
|
1046
|
-
|
|
1047
|
-
<textarea
|
|
1048
|
-
value={content}
|
|
1049
|
-
onChange={(e) => setContent(e.target.value)}
|
|
1050
|
-
placeholder={`---
|
|
1051
|
-
name: skill-name
|
|
1052
|
-
description: What this skill does...
|
|
1053
|
-
---
|
|
1054
|
-
|
|
1055
|
-
# Instructions
|
|
1056
|
-
|
|
1057
|
-
Your skill instructions here...`}
|
|
1058
|
-
rows={16}
|
|
1059
|
-
className="w-full bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-3 py-2 focus:outline-none focus:border-[var(--color-accent)] font-mono text-sm"
|
|
1060
|
-
/>
|
|
1061
|
-
</div>
|
|
1062
|
-
|
|
1063
|
-
<div className="p-6 border-t border-[var(--color-border)] flex justify-end gap-3">
|
|
1064
|
-
<button
|
|
1065
|
-
onClick={onClose}
|
|
1066
|
-
className="px-4 py-2 text-[var(--color-text-secondary)] hover:text-white transition"
|
|
1067
|
-
>
|
|
1068
|
-
Cancel
|
|
1069
|
-
</button>
|
|
1070
|
-
<button
|
|
1071
|
-
onClick={handleImport}
|
|
1072
|
-
disabled={importing}
|
|
1073
|
-
className="bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] disabled:opacity-50 text-black px-4 py-2 rounded font-medium transition"
|
|
1074
|
-
>
|
|
1075
|
-
{importing ? "Importing..." : "Import Skill"}
|
|
1076
|
-
</button>
|
|
1077
|
-
</div>
|
|
1078
|
-
</div>
|
|
1079
|
-
</div>
|
|
1080
|
-
);
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
function ViewSkillModal({
|
|
1084
|
-
skill,
|
|
1085
|
-
authFetch,
|
|
1086
|
-
onClose,
|
|
1087
|
-
onUpdated,
|
|
1088
|
-
}: {
|
|
1089
|
-
skill: Skill;
|
|
1090
|
-
authFetch: (url: string, options?: RequestInit) => Promise<Response>;
|
|
1091
|
-
onClose: () => void;
|
|
1092
|
-
onUpdated: () => void;
|
|
1093
|
-
}) {
|
|
1094
|
-
const [editing, setEditing] = useState(false);
|
|
1095
|
-
const [content, setContent] = useState(skill.content);
|
|
1096
|
-
const [saving, setSaving] = useState(false);
|
|
1097
|
-
|
|
1098
|
-
const handleSave = async () => {
|
|
1099
|
-
setSaving(true);
|
|
1100
|
-
try {
|
|
1101
|
-
await authFetch(`/api/skills/${skill.id}`, {
|
|
1102
|
-
method: "PUT",
|
|
1103
|
-
headers: { "Content-Type": "application/json" },
|
|
1104
|
-
body: JSON.stringify({ content }),
|
|
1105
|
-
});
|
|
1106
|
-
onUpdated();
|
|
1107
|
-
} catch (e) {
|
|
1108
|
-
console.error("Failed to save:", e);
|
|
1109
|
-
}
|
|
1110
|
-
setSaving(false);
|
|
1111
|
-
};
|
|
1112
|
-
|
|
1113
|
-
const handleExport = async () => {
|
|
1114
|
-
try {
|
|
1115
|
-
const res = await authFetch(`/api/skills/${skill.id}/export`);
|
|
1116
|
-
const text = await res.text();
|
|
1117
|
-
const blob = new Blob([text], { type: "text/markdown" });
|
|
1118
|
-
const url = URL.createObjectURL(blob);
|
|
1119
|
-
const a = document.createElement("a");
|
|
1120
|
-
a.href = url;
|
|
1121
|
-
a.download = `${skill.name}-SKILL.md`;
|
|
1122
|
-
a.click();
|
|
1123
|
-
URL.revokeObjectURL(url);
|
|
1124
|
-
} catch (e) {
|
|
1125
|
-
console.error("Failed to export:", e);
|
|
1126
|
-
}
|
|
1127
|
-
};
|
|
1128
|
-
|
|
1129
|
-
return (
|
|
1130
|
-
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4" onClick={onClose}>
|
|
1131
|
-
<div
|
|
1132
|
-
className="bg-[var(--color-surface)] card w-full max-w-3xl max-h-[90vh] overflow-auto"
|
|
1133
|
-
onClick={(e) => e.stopPropagation()}
|
|
1134
|
-
>
|
|
1135
|
-
<div className="p-6 border-b border-[var(--color-border)] flex items-center justify-between">
|
|
1136
|
-
<div>
|
|
1137
|
-
<h2 className="text-xl font-semibold">{skill.name}</h2>
|
|
1138
|
-
<p className="text-sm text-[var(--color-text-muted)] mt-0.5">{skill.description}</p>
|
|
1139
|
-
</div>
|
|
1140
|
-
<div className="flex gap-2">
|
|
1141
|
-
<button
|
|
1142
|
-
onClick={handleExport}
|
|
1143
|
-
className="text-sm text-[var(--color-text-secondary)] hover:text-white transition px-3 py-1 rounded border border-[var(--color-border-light)]"
|
|
1144
|
-
>
|
|
1145
|
-
Export
|
|
1146
|
-
</button>
|
|
1147
|
-
<button
|
|
1148
|
-
onClick={() => setEditing(!editing)}
|
|
1149
|
-
className="text-sm text-[var(--color-text-secondary)] hover:text-white transition px-3 py-1 rounded border border-[var(--color-border-light)]"
|
|
1150
|
-
>
|
|
1151
|
-
{editing ? "View" : "Edit"}
|
|
1152
|
-
</button>
|
|
1153
|
-
</div>
|
|
1154
|
-
</div>
|
|
1155
|
-
|
|
1156
|
-
<div className="p-6">
|
|
1157
|
-
{editing ? (
|
|
1158
|
-
<textarea
|
|
1159
|
-
value={content}
|
|
1160
|
-
onChange={(e) => setContent(e.target.value)}
|
|
1161
|
-
rows={20}
|
|
1162
|
-
className="w-full bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded px-3 py-2 focus:outline-none focus:border-[var(--color-accent)] font-mono text-sm"
|
|
1163
|
-
/>
|
|
1164
|
-
) : (
|
|
1165
|
-
<pre className="bg-[var(--color-bg)] border border-[var(--color-border-light)] rounded p-4 font-mono text-sm overflow-auto max-h-[60vh] whitespace-pre-wrap">
|
|
1166
|
-
{skill.content}
|
|
1167
|
-
</pre>
|
|
1168
|
-
)}
|
|
1169
|
-
</div>
|
|
1170
|
-
|
|
1171
|
-
<div className="p-6 border-t border-[var(--color-border)] flex justify-between">
|
|
1172
|
-
<div className="text-xs text-[var(--color-text-faint)]">
|
|
1173
|
-
{skill.source !== "local" && skill.source_url && (
|
|
1174
|
-
<a href={skill.source_url} target="_blank" rel="noopener noreferrer" className="text-[var(--color-accent)] hover:underline">
|
|
1175
|
-
View source →
|
|
1176
|
-
</a>
|
|
1177
|
-
)}
|
|
1178
|
-
</div>
|
|
1179
|
-
<div className="flex gap-3">
|
|
1180
|
-
<button
|
|
1181
|
-
onClick={onClose}
|
|
1182
|
-
className="px-4 py-2 text-[var(--color-text-secondary)] hover:text-white transition"
|
|
1183
|
-
>
|
|
1184
|
-
Close
|
|
1185
|
-
</button>
|
|
1186
|
-
{editing && (
|
|
1187
|
-
<button
|
|
1188
|
-
onClick={handleSave}
|
|
1189
|
-
disabled={saving}
|
|
1190
|
-
className="bg-[var(--color-accent)] hover:bg-[var(--color-accent-hover)] disabled:opacity-50 text-black px-4 py-2 rounded font-medium transition"
|
|
1191
|
-
>
|
|
1192
|
-
{saving ? "Saving..." : "Save Changes"}
|
|
1193
|
-
</button>
|
|
1194
|
-
)}
|
|
1195
|
-
</div>
|
|
1196
|
-
</div>
|
|
1197
|
-
</div>
|
|
1198
|
-
</div>
|
|
1199
|
-
);
|
|
1200
|
-
}
|