apteva 0.4.4 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -35,12 +35,20 @@ interface MarketplaceSkill {
35
35
  repository: string | null;
36
36
  }
37
37
 
38
+ interface GitHubSkill {
39
+ name: string;
40
+ description: string;
41
+ path: string;
42
+ size: number;
43
+ downloadUrl: string;
44
+ }
45
+
38
46
  export function SkillsPage() {
39
47
  const { authFetch } = useAuth();
40
48
  const { projects, currentProjectId } = useProjects();
41
49
  const [skills, setSkills] = useState<Skill[]>([]);
42
50
  const [loading, setLoading] = useState(true);
43
- const [activeTab, setActiveTab] = useState<"installed" | "marketplace">("installed");
51
+ const [activeTab, setActiveTab] = useState<"installed" | "marketplace" | "github">("installed");
44
52
  const [showCreate, setShowCreate] = useState(false);
45
53
  const [showImport, setShowImport] = useState(false);
46
54
  const [selectedSkill, setSelectedSkill] = useState<Skill | null>(null);
@@ -55,6 +63,17 @@ export function SkillsPage() {
55
63
  const [marketplaceLoading, setMarketplaceLoading] = useState(false);
56
64
  const [installing, setInstalling] = useState<string | null>(null);
57
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
+
58
77
  // Filter skills based on global project selector
59
78
  // When a project is selected, show global + that project's skills
60
79
  const filteredSkills = skills.filter(skill => {
@@ -145,6 +164,125 @@ export function SkillsPage() {
145
164
 
146
165
  const isInstalled = (name: string) => skills.some((s) => s.name === name);
147
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
+
148
286
  return (
149
287
  <>
150
288
  {ConfirmDialog}
@@ -189,6 +327,16 @@ export function SkillsPage() {
189
327
  >
190
328
  Installed ({skills.length})
191
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-[#1a1a1a] text-white"
335
+ : "text-[#666] hover:text-[#888]"
336
+ }`}
337
+ >
338
+ Browse GitHub
339
+ </button>
192
340
  <button
193
341
  onClick={() => setActiveTab("marketplace")}
194
342
  className={`px-4 py-2 rounded text-sm font-medium transition ${
@@ -243,6 +391,187 @@ export function SkillsPage() {
243
391
  </>
244
392
  )}
245
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-[#111] border border-[#333] rounded-lg px-4 py-3 focus:outline-none focus:border-[#f97316]"
411
+ />
412
+ <button
413
+ type="submit"
414
+ disabled={githubLoading}
415
+ className="bg-[#f97316] hover:bg-[#fb923c] 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-[#0a0a0a] border border-[#222] rounded-lg">
424
+ <span className="text-sm text-[#666]">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-[#f97316] hover:underline font-medium"
453
+ >
454
+ {githubRepoInfo.owner}/{githubRepoInfo.repo}
455
+ </a>
456
+ <span className="text-sm text-[#666]">
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-[#1a1a1a] hover:bg-[#222] border border-[#333] hover:border-[#f97316] 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-[#666]">
475
+ Fetching skills from repository...
476
+ </div>
477
+ )}
478
+
479
+ {/* Empty State */}
480
+ {!githubLoading && !githubRepoInfo && !githubError && (
481
+ <div className="bg-[#111] border border-[#1a1a1a] rounded-lg 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-[#666] 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-[#1a1a1a] hover:bg-[#222] border border-[#333] hover:border-[#f97316] 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-[#666]">
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-[#111] border rounded-lg p-4 transition ${
524
+ installed ? "border-green-500/30" : "border-[#1a1a1a] hover:border-[#333]"
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-[#666] 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-[#555]">
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-[#555] px-3 py-1.5">Added</span>
548
+ ) : (
549
+ <button
550
+ onClick={() => installFromGitHub(skill)}
551
+ disabled={isInstalling}
552
+ className="text-sm bg-[#1a1a1a] hover:bg-[#222] border border-[#333] hover:border-[#f97316] 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-[#111] border border-[#1a1a1a] rounded-lg text-sm text-[#666]">
567
+ <p>
568
+ Skills are sourced from GitHub repositories. Each skill should be in its own directory with a{" "}
569
+ <code className="text-[#888] bg-[#0a0a0a] px-1 rounded">SKILL.md</code> file containing instructions.
570
+ </p>
571
+ </div>
572
+ </div>
573
+ )}
574
+
246
575
  {/* Marketplace Tab */}
247
576
  {activeTab === "marketplace" && (
248
577
  <>