diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index 2f25b34e..3cf5173d 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -10,6 +10,14 @@ export function Layout() { const pathname = useRouterState({ select: (s) => s.location.pathname }) const { user, isLoading } = useAuth() + if (pathname === '/registry/skill') { + return ( + + + + ) + } + const navItems: Array<{ label: string to: string diff --git a/web/src/app/router.tsx b/web/src/app/router.tsx index f70f0e1b..f4197a53 100644 --- a/web/src/app/router.tsx +++ b/web/src/app/router.tsx @@ -60,6 +60,7 @@ const SearchPage = createLazyRouteComponent(() => import('@/pages/search'), 'Sea const TermsOfServicePage = createLazyRouteComponent(() => import('@/pages/terms'), 'TermsOfServicePage') const NamespacePage = createLazyRouteComponent(() => import('@/pages/namespace'), 'NamespacePage') const SkillDetailPage = createLazyRouteComponent(() => import('@/pages/skill-detail'), 'SkillDetailPage') +const RegistrySkillPage = createLazyRouteComponent(() => import('@/pages/registry-skill'), 'RegistrySkillPage') const DashboardPage = createLazyRouteComponent(() => import('@/pages/dashboard'), 'DashboardPage') const MySkillsPage = createLazyRouteComponent(() => import('@/pages/dashboard/my-skills'), 'MySkillsPage') const PublishPage = createLazyRouteComponent(() => import('@/pages/dashboard/publish'), 'PublishPage') @@ -214,6 +215,12 @@ const skillDetailRoute = createRoute({ component: SkillDetailPage, }) +const registrySkillRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/registry/skill', + component: RegistrySkillPage, +}) + const dashboardRoute = createRoute({ getParentRoute: () => rootRoute, path: 'dashboard', @@ -360,6 +367,7 @@ const routeTree = rootRoute.addChildren([ termsRoute, namespaceRoute, skillDetailRoute, + registrySkillRoute, dashboardRoute, dashboardSkillsRoute, dashboardPublishRoute, diff --git a/web/src/docs/skill.md b/web/src/docs/skill.md new file mode 100644 index 00000000..ac1bf910 --- /dev/null +++ b/web/src/docs/skill.md @@ -0,0 +1,193 @@ +--- +name: skillhub-registry +description: Use this when you need to search, inspect, install, or publish agent skills against a SkillHub registry. SkillHub is a self-hosted skill registry with a ClawHub-compatible API layer, so prefer the `clawhub` CLI for registry operations instead of making raw HTTP calls. +--- + +# SkillHub Registry + +Use this skill when you need to work with a SkillHub deployment: search skills, inspect metadata, install a package, or publish a new version. + +> Important: Prefer the `clawhub` CLI for registry workflows. SkillHub exposes a ClawHub-compatible API surface and a discovery endpoint at `/.well-known/clawhub.json`, so the CLI is the safest path for auth, resolution, and download behavior. Only fall back to raw HTTP when debugging the server itself. + +## What SkillHub Is + +SkillHub is a self-hosted, enterprise-oriented skill registry. It stores versioned skill packages, supports namespace-based governance, and keeps `SKILL.md` compatibility with OpenSkills-style packages. + +Key facts: + +- Internal coordinates use `@{namespace}/{skill_slug}`. +- ClawHub-compatible clients use a canonical slug instead. +- `latest` always means the latest published version, never draft or pending review. +- Public skills in `@global` can be downloaded anonymously. +- Team namespace skills and non-public skills require authentication. + +## Configure The CLI + +Point `clawhub` at the SkillHub base URL: + +```bash +export CLAWHUB_REGISTRY_URL=https://skillhub.your-company.com +``` + +If you need authenticated access, provide an API token: + +```bash +export CLAWHUB_API_TOKEN=sk_your_api_token_here +``` + +Optional local check: + +```bash +curl https://skillhub.your-company.com/.well-known/clawhub.json +``` + +Expected response: + +```json +{ "apiBase": "/api/v1" } +``` + +## Coordinate Rules + +SkillHub has two naming forms: + +| SkillHub coordinate | Canonical slug for `clawhub` | +|---|---| +| `@global/my-skill` | `my-skill` | +| `@team-name/my-skill` | `team-name--my-skill` | + +Rules: + +- `--` is the namespace separator in the compatibility layer. +- If there is no `--`, the skill is treated as `@global/...`. +- `latest` resolves to the latest published version only. + +Examples: + +```bash +npx clawhub install my-skill +npx clawhub install my-skill@1.2.0 +npx clawhub install team-name--my-skill +``` + +## Common Workflows + +### Search + +```bash +npx clawhub search email +``` + +Use an empty query when you want a broad listing: + +```bash +npx clawhub search "" +``` + +### Inspect A Skill + +```bash +npx clawhub info my-skill +npx clawhub info team-name--my-skill +``` + +### Install + +```bash +npx clawhub install my-skill +npx clawhub install my-skill@1.2.0 +npx clawhub install team-name--my-skill +``` + +### Publish + +Prepare a skill package directory, then publish it: + +```bash +npx clawhub publish ./my-skill +``` + +Publishing requires authentication and sufficient permissions in the target namespace. + +## Authentication And Visibility + +Download and search permissions depend on namespace and visibility: + +- `@global` + `PUBLIC`: anonymous search, inspect, and download are allowed. +- Team namespace + `PUBLIC`: authentication required for download. +- `NAMESPACE_ONLY`: authenticated namespace members only. +- `PRIVATE`: owner or explicitly authorized users only. +- Publish, star, and other write operations always require authentication. + +If a request fails with `403`, check: + +- whether the skill belongs to a team namespace, +- whether the skill is `NAMESPACE_ONLY` or `PRIVATE`, +- whether your token is valid, +- whether you have namespace publish permissions. + +## Skill Package Contract + +SkillHub expects OpenSkills-style packages with `SKILL.md` as the entry point. + +Minimum valid `SKILL.md` frontmatter: + +```yaml +--- +name: my-skill +description: When to use this skill +--- +``` + +Required structure: + +```text +my-skill/ +├── SKILL.md +├── references/ +├── scripts/ +└── assets/ +``` + +Contract notes: + +- `name` and `description` are required. +- `name` becomes the immutable skill slug on first publish. +- `description` becomes the registry summary. +- `references/`, `scripts/`, and `assets/` are optional. +- The package is treated as a text-first resource bundle, not a binary artifact bucket. + +## Publishing Guidance + +Before publishing: + +1. Ensure `SKILL.md` exists at the package root. +2. Keep the skill name in kebab-case. +3. Make sure the version you are publishing is semver-compatible. +4. Avoid relying on `latest` as a rollback tool; SkillHub keeps `latest` automatically pinned to the newest published version. +5. Use custom tags like `beta` or `stable` for release channels when needed. + +## When To Use Raw HTTP + +Use direct HTTP only for server debugging, contract testing, or compatibility work. Relevant endpoints exposed by the current codebase include: + +- `GET /.well-known/clawhub.json` +- `GET /api/v1/search` +- `GET /api/v1/resolve` +- `GET /api/v1/download/{slug}` +- `GET /api/v1/skills/{slug}` +- `POST /api/v1/publish` +- `GET /api/v1/whoami` + +For normal registry usage, stay on the `clawhub` CLI. + +## Project References + +Read these local documents when you need more detail about SkillHub behavior: + +- `docs/00-product-direction.md` +- `docs/06-api-design.md` +- `docs/07-skill-protocol.md` +- `docs/14-skill-lifecycle.md` +- `docs/openclaw-integration.md` +- `README.md` diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index e2bf72d1..a480746a 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -99,8 +99,20 @@ "quickStart": { "title": "Quick Start", "subtitle": "快速开始", - "description": "Get started with SkillHub in just a few simple steps", + "description": "Choose how you want to work, then copy the setup instruction and continue", "tip": "💡 Tip: Visit the skill detail page for one-click install commands with environment variables", + "tabs": { + "agent": "I am Agent", + "human": "I am Human" + }, + "agent": { + "description": "Send a prompt to your Agent to set up the SkillHub Registry", + "command": "Read https://www.example.com/registry/skill and follow the instructions to setup SkillHub Skills Registry" + }, + "human": { + "description": "Use the CLI tool to install Skills", + "command": "npx clawhub search " + }, "steps": { "configureEnv": { "title": "1. Configure Environment Variables", diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index 95fa8998..32a18fd9 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -99,8 +99,20 @@ "quickStart": { "title": "快速开始", "subtitle": "Quick Start", - "description": "只需几个简单步骤,即可开始使用 SkillHub", + "description": "选择你的使用方式,复制说明后继续完成接入", "tip": "💡 提示:访问技能详情页可获取带环境变量的一键复制安装命令", + "tabs": { + "agent": "我是 Agent", + "human": "我是 Human" + }, + "agent": { + "description": "发送提示词给你的 Agent,以设置SkillHub Registry", + "command": "Read https://www.example.com/registry/skill and follow the instructions to setup SkillHub Skills Registry" + }, + "human": { + "description": "使用CLI工具安装Skills", + "command": "npx clawhub search " + }, "steps": { "configureEnv": { "title": "1. 配置环境变量", diff --git a/web/src/pages/landing.tsx b/web/src/pages/landing.tsx index 8910fdc2..862d6456 100644 --- a/web/src/pages/landing.tsx +++ b/web/src/pages/landing.tsx @@ -2,7 +2,7 @@ import { Link, useNavigate } from '@tanstack/react-router' import { useTranslation } from 'react-i18next' import { normalizeSearchQuery } from '@/shared/lib/search-query' import { PackageOpen, Terminal, Shield, Users, GitBranch, Search as SearchIcon, Settings } from 'lucide-react' -import { QuickStartSection } from '@/shared/components/quick-start' +import { LandingQuickStartSection } from '@/shared/components/landing-quick-start' import { SkillCard } from '@/features/skill/skill-card' import { SkeletonList } from '@/shared/components/skeleton-loader' import { useSearchSkills } from '@/shared/hooks/use-skill-queries' @@ -195,7 +195,7 @@ export function LandingPage() { {/* Quick Start */}
- +
{/* Popular Downloads Section */} diff --git a/web/src/pages/registry-skill.tsx b/web/src/pages/registry-skill.tsx new file mode 100644 index 00000000..12c03834 --- /dev/null +++ b/web/src/pages/registry-skill.tsx @@ -0,0 +1,9 @@ +import skillContent from '@/docs/skill.md?raw' + +export function RegistrySkillPage() { + return ( +
+      {skillContent}
+    
+ ) +} diff --git a/web/src/shared/components/landing-quick-start.tsx b/web/src/shared/components/landing-quick-start.tsx new file mode 100644 index 00000000..2d737230 --- /dev/null +++ b/web/src/shared/components/landing-quick-start.tsx @@ -0,0 +1,136 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Bot, Check, Copy, UserRound } from 'lucide-react' + +type LandingQuickStartTabId = 'agent' | 'human' + +interface LandingQuickStartTab { + id: LandingQuickStartTabId + label: string + description: string + command: string +} + +function CompactCopyButton({ text }: { text: string }) { + const { t } = useTranslation() + const [copied, setCopied] = useState(false) + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(text) + setCopied(true) + window.setTimeout(() => setCopied(false), 2000) + } catch (err) { + console.error('Failed to copy:', err) + } + } + + const label = copied ? (t('copyButton.copied') || 'Copied') : (t('copyButton.copy') || 'Copy') + + return ( + + ) +} + +export function LandingQuickStartSection() { + const { t } = useTranslation() + const [activeTab, setActiveTab] = useState('agent') + + const tabs: LandingQuickStartTab[] = [ + { + id: 'agent', + label: t('landing.quickStart.tabs.agent'), + description: t('landing.quickStart.agent.description'), + command: t('landing.quickStart.agent.command'), + }, + { + id: 'human', + label: t('landing.quickStart.tabs.human'), + description: t('landing.quickStart.human.description'), + command: t('landing.quickStart.human.command'), + }, + ] + + const currentTab = tabs.find((tab) => tab.id === activeTab) ?? tabs[0] + + return ( +
+
+
+

+ {t('landing.quickStart.title')} +

+

+ {t('landing.quickStart.description', { defaultValue: t('landing.quickStart.subtitle') })} +

+
+ +
+
+ {tabs.map((tab) => { + const isActive = tab.id === currentTab.id + const Icon = tab.id === 'agent' ? Bot : UserRound + + return ( + + ) + })} +
+ +
+

+ {currentTab.description} +

+ +
+
+ + {currentTab.command} + +
+ +
+
+
+
+
+ ) +} diff --git a/web/src/types/markdown.d.ts b/web/src/types/markdown.d.ts new file mode 100644 index 00000000..212729a1 --- /dev/null +++ b/web/src/types/markdown.d.ts @@ -0,0 +1,4 @@ +declare module '*.md?raw' { + const value: string + export default value +}