fix(skills-catalog): resolve platform metadata fallbacks (#229)

* fix(skills-catalog): resolve platform metadata fallbacks

* fix(skills-catalog): harden platform metadata guards
This commit is contained in:
davida-ps
2026-05-10 16:07:31 +03:00
committed by GitHub
parent 369745821f
commit e4c1e07544
4 changed files with 101 additions and 34 deletions
+21 -10
View File
@@ -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
+42 -6
View File
@@ -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('<!doctype html') || start.startsWith('<html');
};
const isPlatformMetadataObject = (value: unknown): value is SkillPlatformMetadata => {
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
const maybe = value as Record<string, unknown>;
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<SkillJson | null>(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 (
<div className="py-16 text-center">
@@ -180,14 +216,14 @@ export const SkillDetail: React.FC = () => {
{/* Header */}
<section className="flex flex-col md:flex-row md:items-start md:justify-between gap-6">
<div className="flex items-start gap-4">
<span className="text-4xl">{skillData.openclaw?.emoji || '📦'}</span>
<span className="text-4xl">{platformMetadata?.emoji || '📦'}</span>
<div>
<h1 className="text-3xl font-bold text-white mb-1">{skillData.name}</h1>
<div className="flex items-center gap-3 text-sm">
<span className="text-gray-500 font-mono">v{skillData.version}</span>
{/* Category badge - hidden for now, uncomment when we have multiple categories
<span className="text-gray-500 bg-clawd-800 px-2 py-0.5 rounded">
{skillData.openclaw?.category || 'utility'}
{platformMetadata?.category || 'utility'}
</span>
*/}
</div>
@@ -339,16 +375,16 @@ export const SkillDetail: React.FC = () => {
</div>
<div className="flex justify-between">
<dt className="text-gray-500">Category</dt>
<dd className="text-white">{skillData.openclaw?.category}</dd>
<dd className="text-white">{platformMetadata?.category || 'utility'}</dd>
</div>
</dl>
</div>
{skillData.openclaw?.triggers && skillData.openclaw.triggers.length > 0 && (
{triggers.length > 0 && (
<div className="bg-clawd-800/50 border border-clawd-700 rounded-xl p-6 space-y-4">
<h3 className="font-bold text-white">Trigger Phrases</h3>
<div className="flex flex-wrap gap-2">
{skillData.openclaw.triggers.slice(0, 8).map((trigger) => (
{triggers.slice(0, 8).map((trigger) => (
<span
key={trigger}
className="text-xs bg-clawd-700 text-gray-300 px-2 py-1 rounded"
+20 -9
View File
@@ -168,15 +168,26 @@ EOF
echo " ✓ Generated: checksums.json"
# 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,
tag: $tag
}' "$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"),
tag: $tag
}
' "$SKILL_JSON")
# Append to index
if [ "$FIRST_SKILL" = "true" ]; then
+18 -9
View File
@@ -101,6 +101,19 @@ export interface SkillChecksums {
}>;
}
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;
}