apteva 0.4.57 → 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.
Files changed (142) hide show
  1. package/README.md +216 -54
  2. package/cli.js +35 -0
  3. package/install.js +92 -0
  4. package/package.json +12 -79
  5. package/LICENSE +0 -63
  6. package/bin/apteva.js +0 -196
  7. package/dist/ActivityPage.kxzzb4yc.js +0 -3
  8. package/dist/ApiDocsPage.zq998hbm.js +0 -4
  9. package/dist/App.55rea8mn.js +0 -61
  10. package/dist/App.5ywb23z4.js +0 -53
  11. package/dist/App.6thds120.js +0 -4
  12. package/dist/App.9tctxzqm.js +0 -8
  13. package/dist/App.a8r8ttaz.js +0 -4
  14. package/dist/App.agsv5bje.js +0 -4
  15. package/dist/App.cepapqmx.js +0 -4
  16. package/dist/App.dp041gb3.js +0 -221
  17. package/dist/App.fds72zb5.js +0 -4
  18. package/dist/App.fg9qj2dq.js +0 -4
  19. package/dist/App.ndfejbm9.js +0 -4
  20. package/dist/App.nxmfmq1h.js +0 -13
  21. package/dist/App.qdfyt8ba.js +0 -4
  22. package/dist/App.x2d0ygt6.js +0 -4
  23. package/dist/App.yt9p4nr3.js +0 -20
  24. package/dist/App.zn4mw16t.js +0 -1
  25. package/dist/ConnectionsPage.8r96ryw7.js +0 -3
  26. package/dist/McpPage.3cwh0gnd.js +0 -3
  27. package/dist/SettingsPage.ykgdh5ev.js +0 -3
  28. package/dist/SkillsPage.4np1s65b.js +0 -3
  29. package/dist/TasksPage.4g08t7p6.js +0 -3
  30. package/dist/TelemetryPage.72w9pwcp.js +0 -3
  31. package/dist/TestsPage.z4fk3r7r.js +0 -3
  32. package/dist/ThreadsPage.63tcajeh.js +0 -3
  33. package/dist/apteva-kit.css +0 -1
  34. package/dist/icon.png +0 -0
  35. package/dist/index.html +0 -16
  36. package/dist/styles.css +0 -1
  37. package/scripts/postinstall.mjs +0 -102
  38. package/src/auth/index.ts +0 -394
  39. package/src/auth/middleware.ts +0 -213
  40. package/src/binary.ts +0 -536
  41. package/src/channels/index.ts +0 -40
  42. package/src/channels/telegram.ts +0 -311
  43. package/src/crypto.ts +0 -301
  44. package/src/db-tests.ts +0 -174
  45. package/src/db.ts +0 -3133
  46. package/src/integrations/agentdojo.ts +0 -559
  47. package/src/integrations/composio.ts +0 -437
  48. package/src/integrations/index.ts +0 -87
  49. package/src/integrations/skillsmp.ts +0 -318
  50. package/src/mcp-client.ts +0 -605
  51. package/src/mcp-handler.ts +0 -394
  52. package/src/mcp-platform.ts +0 -2403
  53. package/src/openapi.ts +0 -2410
  54. package/src/providers.ts +0 -597
  55. package/src/routes/api/agent-utils.ts +0 -890
  56. package/src/routes/api/agents.ts +0 -916
  57. package/src/routes/api/api-keys.ts +0 -95
  58. package/src/routes/api/channels.ts +0 -182
  59. package/src/routes/api/helpers.ts +0 -12
  60. package/src/routes/api/integrations.ts +0 -639
  61. package/src/routes/api/mcp.ts +0 -574
  62. package/src/routes/api/meta-agent.ts +0 -195
  63. package/src/routes/api/projects.ts +0 -112
  64. package/src/routes/api/providers.ts +0 -424
  65. package/src/routes/api/skills.ts +0 -537
  66. package/src/routes/api/system.ts +0 -333
  67. package/src/routes/api/telemetry.ts +0 -203
  68. package/src/routes/api/tests.ts +0 -148
  69. package/src/routes/api/triggers.ts +0 -518
  70. package/src/routes/api/users.ts +0 -148
  71. package/src/routes/api/webhooks.ts +0 -171
  72. package/src/routes/api.ts +0 -53
  73. package/src/routes/auth.ts +0 -251
  74. package/src/routes/share.ts +0 -86
  75. package/src/routes/static.ts +0 -131
  76. package/src/server.ts +0 -642
  77. package/src/test-runner.ts +0 -598
  78. package/src/triggers/agentdojo.ts +0 -253
  79. package/src/triggers/composio.ts +0 -264
  80. package/src/triggers/index.ts +0 -71
  81. package/src/tui/AgentList.tsx +0 -145
  82. package/src/tui/App.tsx +0 -102
  83. package/src/tui/Login.tsx +0 -104
  84. package/src/tui/api.ts +0 -72
  85. package/src/tui/index.tsx +0 -7
  86. package/src/web/App.tsx +0 -455
  87. package/src/web/components/activity/ActivityPage.tsx +0 -314
  88. package/src/web/components/activity/index.ts +0 -1
  89. package/src/web/components/agents/AgentCard.tsx +0 -189
  90. package/src/web/components/agents/AgentPanel.tsx +0 -2244
  91. package/src/web/components/agents/AgentsView.tsx +0 -180
  92. package/src/web/components/agents/CreateAgentModal.tsx +0 -475
  93. package/src/web/components/agents/index.ts +0 -4
  94. package/src/web/components/api/ApiDocsPage.tsx +0 -842
  95. package/src/web/components/auth/CreateAccountStep.tsx +0 -176
  96. package/src/web/components/auth/LoginPage.tsx +0 -91
  97. package/src/web/components/auth/index.ts +0 -2
  98. package/src/web/components/common/Icons.tsx +0 -250
  99. package/src/web/components/common/LoadingSpinner.tsx +0 -44
  100. package/src/web/components/common/Modal.tsx +0 -199
  101. package/src/web/components/common/Select.tsx +0 -97
  102. package/src/web/components/common/index.ts +0 -20
  103. package/src/web/components/connections/ConnectionsPage.tsx +0 -54
  104. package/src/web/components/connections/IntegrationsTab.tsx +0 -170
  105. package/src/web/components/connections/OverviewTab.tsx +0 -137
  106. package/src/web/components/connections/TriggersTab.tsx +0 -1346
  107. package/src/web/components/dashboard/Dashboard.tsx +0 -572
  108. package/src/web/components/dashboard/index.ts +0 -1
  109. package/src/web/components/index.ts +0 -21
  110. package/src/web/components/layout/ErrorBanner.tsx +0 -18
  111. package/src/web/components/layout/Header.tsx +0 -332
  112. package/src/web/components/layout/Sidebar.tsx +0 -231
  113. package/src/web/components/layout/index.ts +0 -3
  114. package/src/web/components/mcp/IntegrationsPanel.tsx +0 -857
  115. package/src/web/components/mcp/McpPage.tsx +0 -2515
  116. package/src/web/components/mcp/index.ts +0 -1
  117. package/src/web/components/meta-agent/MetaAgent.tsx +0 -245
  118. package/src/web/components/onboarding/OnboardingWizard.tsx +0 -404
  119. package/src/web/components/onboarding/index.ts +0 -1
  120. package/src/web/components/settings/SettingsPage.tsx +0 -2776
  121. package/src/web/components/settings/index.ts +0 -1
  122. package/src/web/components/skills/SkillsPage.tsx +0 -1200
  123. package/src/web/components/tasks/TasksPage.tsx +0 -1116
  124. package/src/web/components/tasks/index.ts +0 -1
  125. package/src/web/components/telemetry/TelemetryPage.tsx +0 -1129
  126. package/src/web/components/tests/TestsPage.tsx +0 -594
  127. package/src/web/components/threads/ThreadsPage.tsx +0 -315
  128. package/src/web/context/AuthContext.tsx +0 -242
  129. package/src/web/context/ProjectContext.tsx +0 -214
  130. package/src/web/context/TelemetryContext.tsx +0 -299
  131. package/src/web/context/ThemeContext.tsx +0 -90
  132. package/src/web/context/UIModeContext.tsx +0 -49
  133. package/src/web/context/index.ts +0 -12
  134. package/src/web/hooks/index.ts +0 -3
  135. package/src/web/hooks/useAgents.ts +0 -115
  136. package/src/web/hooks/useOnboarding.ts +0 -20
  137. package/src/web/hooks/useProviders.ts +0 -75
  138. package/src/web/icon.png +0 -0
  139. package/src/web/index.html +0 -16
  140. package/src/web/styles.css +0 -118
  141. package/src/web/themes.ts +0 -162
  142. 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&#10;&#10;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
- }