import { useEffect, useMemo, useState } from "react"; import { Link, useNavigate, useParams } from "@/lib/router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { CompanySkillDetail, CompanySkillListItem, CompanySkillTrustLevel, } from "@paperclipai/shared"; import { companySkillsApi } from "../api/companySkills"; import { useCompany } from "../context/CompanyContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { useToast } from "../context/ToastContext"; import { queryKeys } from "../lib/queryKeys"; import { EmptyState } from "../components/EmptyState"; import { MarkdownBody } from "../components/MarkdownBody"; import { PageSkeleton } from "../components/PageSkeleton"; import { EntityRow } from "../components/EntityRow"; import { cn } from "../lib/utils"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { ArrowUpRight, BookOpen, Boxes, FolderInput, RefreshCw, ShieldAlert, ShieldCheck, TerminalSquare, } from "lucide-react"; function stripFrontmatter(markdown: string) { const normalized = markdown.replace(/\r\n/g, "\n"); if (!normalized.startsWith("---\n")) return normalized.trim(); const closing = normalized.indexOf("\n---\n", 4); if (closing < 0) return normalized.trim(); return normalized.slice(closing + 5).trim(); } function trustTone(trustLevel: CompanySkillTrustLevel) { switch (trustLevel) { case "markdown_only": return "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300"; case "assets": return "bg-amber-500/10 text-amber-700 dark:text-amber-300"; case "scripts_executables": return "bg-red-500/10 text-red-700 dark:text-red-300"; default: return "bg-muted text-muted-foreground"; } } function trustLabel(trustLevel: CompanySkillTrustLevel) { switch (trustLevel) { case "markdown_only": return "Markdown only"; case "assets": return "Assets"; case "scripts_executables": return "Scripts"; default: return trustLevel; } } function compatibilityLabel(detail: CompanySkillDetail | CompanySkillListItem) { switch (detail.compatibility) { case "compatible": return "Compatible"; case "unknown": return "Unknown"; case "invalid": return "Invalid"; default: return detail.compatibility; } } function SkillListItem({ skill, selected, }: { skill: CompanySkillListItem; selected: boolean; }) { return (
{skill.name} {skill.slug}
{skill.description && (

{skill.description}

)}
{trustLabel(skill.trustLevel)}
{skill.attachedAgentCount} agent{skill.attachedAgentCount === 1 ? "" : "s"} {skill.fileInventory.length} file{skill.fileInventory.length === 1 ? "" : "s"}
); } function SkillDetailPanel({ detail, isLoading, }: { detail: CompanySkillDetail | null | undefined; isLoading: boolean; }) { if (isLoading) { return ; } if (!detail) { return (

Select a skill

Review its markdown, inspect files, and see which agents have it attached.

); } const markdownBody = stripFrontmatter(detail.markdown); return (

{detail.name}

{detail.slug} {trustLabel(detail.trustLevel)}
{detail.description && (

{detail.description}

)}
{compatibilityLabel(detail)} {detail.attachedAgentCount} attached agent{detail.attachedAgentCount === 1 ? "" : "s"}

SKILL.md

{detail.sourceLocator?.startsWith("http") ? ( Open source ) : detail.sourceLocator ? ( {detail.sourceLocator} ) : null}
{markdownBody}

Inventory

{detail.fileInventory.map((entry) => (
{entry.path} {entry.kind}
))}

Used By Agents

{detail.usedByAgents.length === 0 ? (

No agents are currently attached to this skill.

) : (
{detail.usedByAgents.map((agent) => ( {agent.actualState} ) : undefined} /> ))}
)}
); } export function CompanySkills() { const { skillId } = useParams<{ skillId?: string }>(); const navigate = useNavigate(); const queryClient = useQueryClient(); const { selectedCompanyId } = useCompany(); const { setBreadcrumbs } = useBreadcrumbs(); const { pushToast } = useToast(); const [source, setSource] = useState(""); useEffect(() => { setBreadcrumbs([ { label: "Skills", href: "/skills" }, ...(skillId ? [{ label: "Detail" }] : []), ]); }, [setBreadcrumbs, skillId]); const { data: skills, isLoading, error, } = useQuery({ queryKey: queryKeys.companySkills.list(selectedCompanyId ?? ""), queryFn: () => companySkillsApi.list(selectedCompanyId!), enabled: Boolean(selectedCompanyId), }); const selectedSkillId = useMemo(() => { if (!skillId) return skills?.[0]?.id ?? null; return skillId; }, [skillId, skills]); const { data: detail, isLoading: detailLoading, } = useQuery({ queryKey: queryKeys.companySkills.detail(selectedCompanyId ?? "", selectedSkillId ?? ""), queryFn: () => companySkillsApi.detail(selectedCompanyId!, selectedSkillId!), enabled: Boolean(selectedCompanyId && selectedSkillId), }); const importSkill = useMutation({ mutationFn: (importSource: string) => companySkillsApi.importFromSource(selectedCompanyId!, importSource), onSuccess: async (result) => { await queryClient.invalidateQueries({ queryKey: queryKeys.companySkills.list(selectedCompanyId!) }); if (result.imported[0]) { navigate(`/skills/${result.imported[0].id}`); } pushToast({ tone: "success", title: "Skills imported", body: `${result.imported.length} skill${result.imported.length === 1 ? "" : "s"} added to the company library.`, }); if (result.warnings[0]) { pushToast({ tone: "warn", title: "Import warnings", body: result.warnings[0], }); } setSource(""); }, onError: (importError) => { pushToast({ tone: "error", title: "Skill import failed", body: importError instanceof Error ? importError.message : "Failed to import skill source.", }); }, }); if (!selectedCompanyId) { return ; } return (
Company skill library

Manage reusable skills once, attach them anywhere.

Import `SKILL.md` packages from local paths, GitHub repos, or direct URLs. Agents attach by skill shortname, while adapters decide how those skills are installed or mounted.

Markdown-first

`skills.sh` compatible packages stay readable and repo-native.

GitHub aware

Import a repo, a subtree, or a single skill file without a registry.

Trust surfaced

Scripts and executable bundles stay visible instead of being hidden in setup.

setSource(event.target.value)} placeholder="Path, GitHub URL, npx skills add ..., or owner/repo/skill" className="h-10" />
{error &&

{error.message}

} {!isLoading && (skills?.length ?? 0) === 0 ? ( { const trimmed = source.trim(); if (trimmed) importSkill.mutate(trimmed); }} /> ) : (

Library

{skills?.length ?? 0} tracked skill{(skills?.length ?? 0) === 1 ? "" : "s"}

{isLoading ? ( ) : (
{(skills ?? []).map((skill) => ( ))}
)}
)}
); }