From e4c1e0754489a0d374c9449d9d1e2a80e6b41273 Mon Sep 17 00:00:00 2001 From: davida-ps Date: Sun, 10 May 2026 16:07:31 +0300 Subject: [PATCH] fix(skills-catalog): resolve platform metadata fallbacks (#229) * fix(skills-catalog): resolve platform metadata fallbacks * fix(skills-catalog): harden platform metadata guards --- .github/workflows/deploy-pages.yml | 31 ++++++++++++------- pages/SkillDetail.tsx | 48 ++++++++++++++++++++++++++---- scripts/populate-local-skills.sh | 29 ++++++++++++------ types.ts | 27 +++++++++++------ 4 files changed, 101 insertions(+), 34 deletions(-) diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index 9b944d7..71ad8f8 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -183,16 +183,27 @@ jobs: done # Build skill entry for index - SKILL_DATA=$(jq -c --arg tag "$TAG" '{ - id: .name, - name: .name, - version: .version, - description: .description, - emoji: .openclaw.emoji, - category: .openclaw.category, - trust: .trust.level, - tag: $tag - }' "$MIRROR_DIR/skill.json") + SKILL_DATA=$(jq -c --arg tag "$TAG" ' + def object_or_empty($value): + if ($value | type) == "object" then $value else {} end; + def object_field($name): + object_or_empty(.[$name]?); + def platform_meta: + (.platform as $platform + | if ($platform | type) == "string" then object_or_empty(.[$platform]?) + else {} + end); + { + id: .name, + name: .name, + version: .version, + description: .description, + emoji: (platform_meta.emoji // object_field("openclaw").emoji // object_field("hermes").emoji // object_field("nanoclaw").emoji // object_field("picoclaw").emoji // "📦"), + category: (platform_meta.category // object_field("openclaw").category // object_field("hermes").category // object_field("nanoclaw").category // object_field("picoclaw").category // "utility"), + trust: .trust.level, + tag: $tag + } + ' "$MIRROR_DIR/skill.json") # Append to index (handle first entry without comma) if [ -f "public/skills/.first_done" ]; then diff --git a/pages/SkillDetail.tsx b/pages/SkillDetail.tsx index 18b4fcd..5a04562 100644 --- a/pages/SkillDetail.tsx +++ b/pages/SkillDetail.tsx @@ -4,15 +4,41 @@ import { ArrowLeft, Copy, Check, Download, ExternalLink, FileText, Shield } from import Markdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { Footer } from '../components/Footer'; -import type { SkillJson, SkillChecksums } from '../types'; +import type { SkillJson, SkillChecksums, SkillPlatformMetadata } from '../types'; import { defaultMarkdownComponents } from '../utils/markdownComponents'; import { stripFrontmatter } from '../utils/markdownHelpers.mjs'; +const PLATFORM_METADATA_KEYS = ['openclaw', 'hermes', 'nanoclaw', 'picoclaw'] as const; + const isProbablyHtmlDocument = (text: string): boolean => { const start = text.trimStart().slice(0, 200).toLowerCase(); return start.startsWith(' { + if (!value || typeof value !== 'object' || Array.isArray(value)) return false; + const maybe = value as Record; + return 'emoji' in maybe || 'category' in maybe || 'triggers' in maybe; +}; + +const resolvePlatformMetadata = (skill: SkillJson): SkillPlatformMetadata => { + const platform = skill.platform; + if ( + typeof platform === 'string' && + (PLATFORM_METADATA_KEYS as readonly string[]).includes(platform) + ) { + const platformBlock = skill[platform as (typeof PLATFORM_METADATA_KEYS)[number]]; + if (isPlatformMetadataObject(platformBlock)) return platformBlock; + } + + for (const key of PLATFORM_METADATA_KEYS) { + const fallbackBlock = skill[key]; + if (isPlatformMetadataObject(fallbackBlock)) return fallbackBlock; + } + + return {}; +}; + export const SkillDetail: React.FC = () => { const { skillId } = useParams<{ skillId: string }>(); const [skillData, setSkillData] = useState(null); @@ -144,6 +170,16 @@ export const SkillDetail: React.FC = () => { return skillData.homepage; }, [skillData]); + const platformMetadata = useMemo( + () => (skillData ? resolvePlatformMetadata(skillData) : null), + [skillData] + ); + + const triggers = useMemo(() => { + if (!platformMetadata || !Array.isArray(platformMetadata.triggers)) return []; + return platformMetadata.triggers; + }, [platformMetadata]); + if (loading) { return (
@@ -180,14 +216,14 @@ export const SkillDetail: React.FC = () => { {/* Header */}
- {skillData.openclaw?.emoji || '📦'} + {platformMetadata?.emoji || '📦'}

{skillData.name}

v{skillData.version} {/* Category badge - hidden for now, uncomment when we have multiple categories - {skillData.openclaw?.category || 'utility'} + {platformMetadata?.category || 'utility'} */}
@@ -339,16 +375,16 @@ export const SkillDetail: React.FC = () => {
Category
-
{skillData.openclaw?.category}
+
{platformMetadata?.category || 'utility'}
- {skillData.openclaw?.triggers && skillData.openclaw.triggers.length > 0 && ( + {triggers.length > 0 && (

Trigger Phrases

- {skillData.openclaw.triggers.slice(0, 8).map((trigger) => ( + {triggers.slice(0, 8).map((trigger) => ( ; } +export interface SkillPlatformMetadata { + emoji?: string; + category?: string; + feed_url?: string; + requires?: { + bins?: string[]; + [key: string]: unknown; + }; + triggers?: string[]; + internal?: boolean; + [key: string]: unknown; +} + export interface SkillJson { name: string; version: string; @@ -116,13 +129,9 @@ export interface SkillJson { description: string; }>; }; - openclaw: { - emoji: string; - category: string; - feed_url?: string; - requires?: { - bins?: string[]; - }; - triggers: string[]; - }; + platform?: CorePlatformSlug | (string & {}); + openclaw?: SkillPlatformMetadata | null; + hermes?: SkillPlatformMetadata | null; + nanoclaw?: SkillPlatformMetadata | null; + picoclaw?: SkillPlatformMetadata | null; }