feat(skills): add Hermes-style skill explorer (#3388)

This commit is contained in:
Steven Enamakel
2026-06-05 11:12:27 -04:00
committed by GitHub
parent 5fa8785274
commit 298c3e5b23
39 changed files with 702 additions and 26 deletions
@@ -32,6 +32,9 @@ vi.mock('../../skills/CreateSkillModal', () => ({
version: '',
author: null,
tags: [],
platforms: [],
relatedSkills: [],
sourceFormat: 'openhuman',
tools: [],
prompts: [],
location: null,
@@ -66,6 +69,9 @@ const seeded = (overrides: Partial<SkillSummary>): SkillSummary => ({
version: '0.1.0',
author: null,
tags: [],
platforms: [],
relatedSkills: [],
sourceFormat: 'openhuman',
tools: [],
prompts: [],
location: null,
@@ -29,6 +29,9 @@ const KNOWN_GROUPS: Array<{ prefix: string; key: string }> = [
{ prefix: 'scripts/', key: 'scripts' },
{ prefix: 'references/', key: 'references' },
{ prefix: 'assets/', key: 'assets' },
{ prefix: 'templates/', key: 'templates' },
{ prefix: 'examples/', key: 'examples' },
{ prefix: 'prompts/', key: 'prompts' },
];
function groupResources(resources: string[]): ResourceGroup[] {
@@ -74,6 +77,9 @@ const GROUP_LABEL_KEYS: Record<string, string> = {
scripts: 'skills.resource.tree.scripts',
references: 'skills.resource.tree.references',
assets: 'skills.resource.tree.assets',
templates: 'skills.resource.tree.templates',
examples: 'skills.resource.tree.examples',
prompts: 'skills.resource.tree.prompts',
other: 'skills.resource.tree.other',
};
@@ -33,6 +33,9 @@ function builtSkill(overrides: Partial<SkillSummary> = {}): SkillSummary {
version: '',
author: null,
tags: [],
platforms: [],
relatedSkills: [],
sourceFormat: 'openhuman',
tools: [],
prompts: [],
location: '/home/u/.openhuman/skills/my-skill/SKILL.md',
@@ -372,6 +372,9 @@ describe('CreateWorkflowForm — edit mode', () => {
version: '',
author: 'me',
tags: ['t1'],
platforms: [],
relatedSkills: [],
sourceFormat: 'openhuman',
tools: ['shell'],
prompts: [],
location: null,
@@ -29,6 +29,9 @@ const fixture: SkillSummary = {
version: '',
author: null,
tags: [],
platforms: [],
relatedSkills: [],
sourceFormat: 'openhuman',
tools: [],
prompts: [],
location: '/Users/me/.openhuman/skills/weather-helper/SKILL.md',
+20
View File
@@ -254,6 +254,15 @@ const messages: TranslationMap = {
'skills.available': 'متاح',
'skills.addAccount': 'إضافة حساب',
'skills.channels': 'القنوات',
'skills.explorer.emptyCta': 'التثبيت من رابط',
'skills.explorer.emptyDescription':
'ثبّت حزمة SKILL.md أو ضع مجلدات بنمط Hermes داخل ~/.openhuman/skills.',
'skills.explorer.emptyTitle': 'لم يتم العثور على مهارات',
'skills.explorer.installFromUrl': 'التثبيت من رابط',
'skills.explorer.newSkill': 'مهارة جديدة',
'skills.explorer.subtitle':
'تصفح حزم SKILL.md، بما في ذلك markdown والسكربتات والمراجع والقوالب والأمثلة والمطالبات بنمط Hermes.',
'skills.explorer.title': 'مستكشف المهارات',
'skills.integrations': 'التكاملات',
'skills.integrationsSubtitle':
'اتصالات OAuth السحابية — سجّل الدخول بحسابك ويتولى Composio إدارة الرموز حتى يتمكن الوكلاء من القراءة والتصرف نيابةً عنك. لا حاجة لإدارة مفاتيح API.',
@@ -263,6 +272,7 @@ const messages: TranslationMap = {
'skills.composio.noApiKeyCta': 'افتح في الإعدادات',
'skills.tabs.composio': 'Composio',
'skills.tabs.channels': 'القنوات',
'skills.tabs.explorer': 'المهارات',
'skills.tabs.mcp': 'MCP الخوادم',
'memory.title': 'الذاكرة',
'memory.search': 'البحث في الذكريات...',
@@ -3974,6 +3984,9 @@ const messages: TranslationMap = {
'skills.detail.closeAriaLabel': 'إغلاق تفاصيل المهارة',
'skills.detail.location': 'الموقع',
'skills.detail.noBundledResources': 'لا توجد موارد مجمّعة.',
'skills.detail.platforms': 'المنصات',
'skills.detail.relatedSkills': 'مهارات ذات صلة',
'skills.detail.sourceFormat': 'التنسيق',
'skills.detail.tags': 'العلامات',
'skills.detail.warnings': 'التحذيرات',
'skills.install.errors.alreadyInstalledHint':
@@ -4075,6 +4088,13 @@ const messages: TranslationMap = {
'skills.resource.preview.closeAriaLabel': 'إغلاق المعاينة',
'skills.resource.preview.failed': 'فشلت المعاينة',
'skills.resource.preview.loading': 'جارٍ تحميل المعاينة…',
'skills.resource.tree.assets': 'الأصول',
'skills.resource.tree.examples': 'أمثلة',
'skills.resource.tree.other': 'أخرى',
'skills.resource.tree.prompts': 'المطالبات',
'skills.resource.tree.references': 'المراجع',
'skills.resource.tree.scripts': 'السكربتات',
'skills.resource.tree.templates': 'القوالب',
'skills.resource.tree.empty': 'لا توجد موارد مجمّعة.',
'skills.search.placeholder': 'بحث',
'skills.setup.autocomplete.acceptKey': 'مفتاح القبول',
+20
View File
@@ -256,6 +256,15 @@ const messages: TranslationMap = {
'skills.available': 'পাওয়া যাচ্ছে',
'skills.addAccount': 'অ্যাকাউন্ট যোগ করুন',
'skills.channels': 'চ্যানেল',
'skills.explorer.emptyCta': 'URL থেকে ইনস্টল করুন',
'skills.explorer.emptyDescription':
'একটি SKILL.md প্যাকেজ ইনস্টল করুন বা Hermes-ধাঁচের ফোল্ডার ~/.openhuman/skills-এ রাখুন।',
'skills.explorer.emptyTitle': 'কোনো স্কিল পাওয়া যায়নি',
'skills.explorer.installFromUrl': 'URL থেকে ইনস্টল করুন',
'skills.explorer.newSkill': 'নতুন স্কিল',
'skills.explorer.subtitle':
'Hermes-ধাঁচের markdown, scripts, references, templates, examples এবং prompts সহ SKILL.md প্যাকেজ ব্রাউজ করুন।',
'skills.explorer.title': 'স্কিল এক্সপ্লোরার',
'skills.integrations': 'ইন্টিগ্রেশন',
'skills.integrationsSubtitle':
'ক্লাউড-ভিত্তিক OAuth সংযোগ — আপনার অ্যাকাউন্ট দিয়ে সাইন ইন করুন এবং Composio টোকেন পরিচালনা করে যাতে এজেন্টরা আপনার পক্ষে পড়তে এবং কাজ করতে পারে। কোনো API কী পরিচালনা করতে হবে না।',
@@ -265,6 +274,7 @@ const messages: TranslationMap = {
'skills.composio.noApiKeyCta': 'সেটিংসে খুলুন',
'skills.tabs.composio': 'Composio',
'skills.tabs.channels': 'চ্যানেল',
'skills.tabs.explorer': 'স্কিল',
'skills.tabs.mcp': 'MCP সার্ভার',
'memory.title': 'মেমোরি',
'memory.search': 'মেমোরি খুঁজুন...',
@@ -4048,6 +4058,9 @@ const messages: TranslationMap = {
'skills.detail.closeAriaLabel': 'স্কিলের বিবরণ বন্ধ করুন',
'skills.detail.location': 'লোকেশন',
'skills.detail.noBundledResources': 'কোনো বান্ডেলড রিসোর্স নেই।',
'skills.detail.platforms': 'প্ল্যাটফর্ম',
'skills.detail.relatedSkills': 'সম্পর্কিত স্কিল',
'skills.detail.sourceFormat': 'ফরম্যাট',
'skills.detail.tags': 'ট্যাগ',
'skills.detail.warnings': 'সতর্কতা',
'skills.install.errors.alreadyInstalledHint':
@@ -4151,6 +4164,13 @@ const messages: TranslationMap = {
'skills.resource.preview.closeAriaLabel': 'প্রিভিউ বন্ধ করুন',
'skills.resource.preview.failed': 'প্রিভিউ ব্যর্থ',
'skills.resource.preview.loading': 'প্রিভিউ লোড হচ্ছে…',
'skills.resource.tree.assets': 'অ্যাসেট',
'skills.resource.tree.examples': 'উদাহরণ',
'skills.resource.tree.other': 'অন্যান্য',
'skills.resource.tree.prompts': 'প্রম্পট',
'skills.resource.tree.references': 'রেফারেন্স',
'skills.resource.tree.scripts': 'স্ক্রিপ্ট',
'skills.resource.tree.templates': 'টেমপ্লেট',
'skills.resource.tree.empty': 'কোনো বান্ডেলড রিসোর্স নেই।',
'skills.search.placeholder': 'প্লেসহোল্ডার',
'skills.setup.autocomplete.acceptKey': 'অ্যাক্সেপ্ট কী',
+20
View File
@@ -265,6 +265,15 @@ const messages: TranslationMap = {
'skills.available': 'Verfügbar',
'skills.addAccount': 'Konto hinzufügen',
'skills.channels': 'Kanäle',
'skills.explorer.emptyCta': 'Von URL installieren',
'skills.explorer.emptyDescription':
'Installiere ein SKILL.md-Paket oder lege Hermes-artige Ordner unter ~/.openhuman/skills ab.',
'skills.explorer.emptyTitle': 'Keine Skills gefunden',
'skills.explorer.installFromUrl': 'Von URL installieren',
'skills.explorer.newSkill': 'Neuer Skill',
'skills.explorer.subtitle':
'Durchsuche SKILL.md-Pakete, einschließlich Hermes-artigem Markdown, Skripten, Referenzen, Vorlagen, Beispielen und Prompts.',
'skills.explorer.title': 'Skill-Explorer',
'skills.integrations': 'Integrationen',
'skills.integrationsSubtitle':
'Cloud-basierte OAuth-Verbindungen mit deinem Konto anmelden und Composio verwaltet die Tokens, damit Agenten in deinem Namen lesen und handeln können. Keine API-Schlüssel notwendig.',
@@ -274,6 +283,7 @@ const messages: TranslationMap = {
'skills.composio.noApiKeyCta': 'In den Einstellungen öffnen',
'skills.tabs.composio': 'Composio',
'skills.tabs.channels': 'Kanäle',
'skills.tabs.explorer': 'Skills',
'skills.tabs.mcp': 'MCP Server',
'memory.title': 'Erinnerung',
'memory.search': 'Erinnerungen suchen...',
@@ -4155,6 +4165,9 @@ const messages: TranslationMap = {
'skills.detail.closeAriaLabel': 'Fertigkeitsdetails schließen',
'skills.detail.location': 'Standort',
'skills.detail.noBundledResources': 'Keine gebündelten Ressourcen.',
'skills.detail.platforms': 'Plattformen',
'skills.detail.relatedSkills': 'Verwandte Skills',
'skills.detail.sourceFormat': 'Format',
'skills.detail.tags': 'Schlagworte',
'skills.detail.warnings': 'Warnungen',
'skills.install.errors.alreadyInstalledHint':
@@ -4260,6 +4273,13 @@ const messages: TranslationMap = {
'skills.resource.preview.closeAriaLabel': 'Vorschau schließen',
'skills.resource.preview.failed': 'Vorschau fehlgeschlagen',
'skills.resource.preview.loading': 'Vorschau wird geladen…',
'skills.resource.tree.assets': 'Assets',
'skills.resource.tree.examples': 'Beispiele',
'skills.resource.tree.other': 'Andere',
'skills.resource.tree.prompts': 'Prompts',
'skills.resource.tree.references': 'Referenzen',
'skills.resource.tree.scripts': 'Skripte',
'skills.resource.tree.templates': 'Vorlagen',
'skills.resource.tree.empty': 'Keine gebündelten Ressourcen.',
'skills.search.placeholder': 'Platzhalter',
'skills.setup.autocomplete.acceptKey': 'Schlüssel akzeptieren',
+20
View File
@@ -281,6 +281,15 @@ const en: TranslationMap = {
'skills.available': 'Available',
'skills.addAccount': 'Add Account',
'skills.channels': 'Channels',
'skills.explorer.emptyCta': 'Install from URL',
'skills.explorer.emptyDescription':
'Install a SKILL.md package or place Hermes-style folders under ~/.openhuman/skills.',
'skills.explorer.emptyTitle': 'No skills found',
'skills.explorer.installFromUrl': 'Install from URL',
'skills.explorer.newSkill': 'New skill',
'skills.explorer.subtitle':
'Browse SKILL.md packages, including Hermes-style markdown, scripts, references, templates, examples, and prompts.',
'skills.explorer.title': 'Skills explorer',
'skills.integrations': 'Composio Integrations',
'skills.integrationsSubtitle':
'Cloud-based OAuth connections — sign in with your account and Composio brokers the tokens so agents can read and act on your behalf. No API keys to manage.',
@@ -290,6 +299,7 @@ const en: TranslationMap = {
'skills.composio.noApiKeyCta': 'Open in Settings',
'skills.tabs.composio': 'Composio',
'skills.tabs.channels': 'Channels',
'skills.tabs.explorer': 'Skills',
'skills.tabs.mcp': 'MCP Servers',
// Intelligence / Memory
'memory.title': 'Memory',
@@ -4578,6 +4588,9 @@ const en: TranslationMap = {
'skills.detail.closeAriaLabel': 'Close skill details',
'skills.detail.location': 'Location',
'skills.detail.noBundledResources': 'No bundled resources.',
'skills.detail.platforms': 'Platforms',
'skills.detail.relatedSkills': 'Related skills',
'skills.detail.sourceFormat': 'Format',
'skills.detail.tags': 'Tags',
'skills.detail.warnings': 'Warnings',
'skills.install.errors.alreadyInstalledHint':
@@ -4683,7 +4696,14 @@ const en: TranslationMap = {
'skills.resource.preview.closeAriaLabel': 'Close preview',
'skills.resource.preview.failed': 'Preview failed',
'skills.resource.preview.loading': 'Loading preview…',
'skills.resource.tree.assets': 'Assets',
'skills.resource.tree.empty': 'No bundled resources.',
'skills.resource.tree.examples': 'Examples',
'skills.resource.tree.other': 'Other',
'skills.resource.tree.prompts': 'Prompts',
'skills.resource.tree.references': 'References',
'skills.resource.tree.scripts': 'Scripts',
'skills.resource.tree.templates': 'Templates',
'skills.search.placeholder': 'Search skills…',
'skills.setup.autocomplete.acceptKey': 'Accept key',
'skills.setup.autocomplete.activeDesc': 'Inline completions are running and ready.',
+20
View File
@@ -266,6 +266,15 @@ const messages: TranslationMap = {
'skills.available': 'Disponible',
'skills.addAccount': 'Agregar cuenta',
'skills.channels': 'Canales',
'skills.explorer.emptyCta': 'Instalar desde URL',
'skills.explorer.emptyDescription':
'Instala un paquete SKILL.md o coloca carpetas estilo Hermes en ~/.openhuman/skills.',
'skills.explorer.emptyTitle': 'No se encontraron skills',
'skills.explorer.installFromUrl': 'Instalar desde URL',
'skills.explorer.newSkill': 'Nueva skill',
'skills.explorer.subtitle':
'Explora paquetes SKILL.md, incluidos markdown, scripts, referencias, plantillas, ejemplos y prompts estilo Hermes.',
'skills.explorer.title': 'Explorador de skills',
'skills.integrations': 'Integraciones',
'skills.integrationsSubtitle':
'Conexiones OAuth en la nube: inicia sesión con tu cuenta y Composio gestiona los tokens para que los agentes puedan leer y actuar en tu nombre. Sin claves de API que administrar.',
@@ -275,6 +284,7 @@ const messages: TranslationMap = {
'skills.composio.noApiKeyCta': 'Abrir en Ajustes',
'skills.tabs.composio': 'Composio',
'skills.tabs.channels': 'Canales',
'skills.tabs.explorer': 'Skills',
'skills.tabs.mcp': 'MCP Servidores',
'memory.title': 'Memoria',
'memory.search': 'Buscar recuerdos...',
@@ -4128,6 +4138,9 @@ const messages: TranslationMap = {
'skills.detail.closeAriaLabel': 'Cerrar detalles del skill',
'skills.detail.location': 'Ubicación',
'skills.detail.noBundledResources': 'Sin recursos incluidos.',
'skills.detail.platforms': 'Plataformas',
'skills.detail.relatedSkills': 'Skills relacionadas',
'skills.detail.sourceFormat': 'Formato',
'skills.detail.tags': 'Etiquetas',
'skills.detail.warnings': 'Advertencias',
'skills.install.errors.alreadyInstalledHint':
@@ -4234,6 +4247,13 @@ const messages: TranslationMap = {
'skills.resource.preview.closeAriaLabel': 'Cerrar vista previa',
'skills.resource.preview.failed': 'Vista previa fallida',
'skills.resource.preview.loading': 'Cargando vista previa…',
'skills.resource.tree.assets': 'Recursos',
'skills.resource.tree.examples': 'Ejemplos',
'skills.resource.tree.other': 'Otros',
'skills.resource.tree.prompts': 'Prompts',
'skills.resource.tree.references': 'Referencias',
'skills.resource.tree.scripts': 'Scripts',
'skills.resource.tree.templates': 'Plantillas',
'skills.resource.tree.empty': 'Sin recursos incluidos.',
'skills.search.placeholder': 'Marcador de posición',
'skills.setup.autocomplete.acceptKey': 'Tecla de aceptación',
+20
View File
@@ -264,6 +264,15 @@ const messages: TranslationMap = {
'skills.available': 'Disponible',
'skills.addAccount': 'Ajouter un compte',
'skills.channels': 'Canaux',
'skills.explorer.emptyCta': 'Installer depuis une URL',
'skills.explorer.emptyDescription':
'Installez un paquet SKILL.md ou placez des dossiers de style Hermes dans ~/.openhuman/skills.',
'skills.explorer.emptyTitle': 'Aucune skill trouvée',
'skills.explorer.installFromUrl': 'Installer depuis une URL',
'skills.explorer.newSkill': 'Nouvelle skill',
'skills.explorer.subtitle':
'Parcourez les paquets SKILL.md, y compris markdown, scripts, références, modèles, exemples et prompts de style Hermes.',
'skills.explorer.title': 'Explorateur de skills',
'skills.integrations': 'Intégrations',
'skills.integrationsSubtitle':
'Connexions OAuth cloud — connectez-vous avec votre compte et Composio gère les jetons pour que les agents puissent lire et agir en votre nom. Aucune clé API à gérer.',
@@ -273,6 +282,7 @@ const messages: TranslationMap = {
'skills.composio.noApiKeyCta': 'Ouvrir dans les paramètres',
'skills.tabs.composio': 'Composio',
'skills.tabs.channels': 'Canaux',
'skills.tabs.explorer': 'Skills',
'skills.tabs.mcp': 'MCP Serveurs',
'memory.title': 'Mémoire',
'memory.search': 'Rechercher dans la mémoire…',
@@ -4140,6 +4150,9 @@ const messages: TranslationMap = {
'skills.detail.closeAriaLabel': 'Fermer les détails de la compétence',
'skills.detail.location': 'Emplacement',
'skills.detail.noBundledResources': 'Aucune ressource groupée.',
'skills.detail.platforms': 'Plateformes',
'skills.detail.relatedSkills': 'Skills associées',
'skills.detail.sourceFormat': 'Format',
'skills.detail.tags': 'Étiquettes',
'skills.detail.warnings': 'Avertissements',
'skills.install.errors.alreadyInstalledHint':
@@ -4246,6 +4259,13 @@ const messages: TranslationMap = {
'skills.resource.preview.closeAriaLabel': "Fermer l'aperçu",
'skills.resource.preview.failed': "Échec de l'aperçu",
'skills.resource.preview.loading': "Chargement de l'aperçu…",
'skills.resource.tree.assets': 'Ressources',
'skills.resource.tree.examples': 'Exemples',
'skills.resource.tree.other': 'Autres',
'skills.resource.tree.prompts': 'Prompts',
'skills.resource.tree.references': 'Références',
'skills.resource.tree.scripts': 'Scripts',
'skills.resource.tree.templates': 'Modèles',
'skills.resource.tree.empty': 'Aucune ressource groupée.',
'skills.search.placeholder': 'Espace réservé',
'skills.setup.autocomplete.acceptKey': "Touche d'acceptation",
+20
View File
@@ -256,6 +256,15 @@ const messages: TranslationMap = {
'skills.available': 'उपलब्ध',
'skills.addAccount': 'अकाउंट जोड़ें',
'skills.channels': 'चैनल',
'skills.explorer.emptyCta': 'URL से इंस्टॉल करें',
'skills.explorer.emptyDescription':
'SKILL.md पैकेज इंस्टॉल करें या Hermes-शैली फ़ोल्डर ~/.openhuman/skills में रखें।',
'skills.explorer.emptyTitle': 'कोई स्किल नहीं मिली',
'skills.explorer.installFromUrl': 'URL से इंस्टॉल करें',
'skills.explorer.newSkill': 'नई स्किल',
'skills.explorer.subtitle':
'SKILL.md पैकेज ब्राउज़ करें, जिनमें Hermes-शैली markdown, scripts, references, templates, examples और prompts शामिल हैं।',
'skills.explorer.title': 'स्किल एक्सप्लोरर',
'skills.integrations': 'इंटीग्रेशन',
'skills.integrationsSubtitle':
'क्लाउड-आधारित OAuth कनेक्शन — अपने अकाउंट से साइन इन करें और Composio टोकन ब्रोकर करता है ताकि एजेंट आपकी ओर से पढ़ और कार्य कर सकें। कोई API कुंजी प्रबंधित नहीं करनी।',
@@ -265,6 +274,7 @@ const messages: TranslationMap = {
'skills.composio.noApiKeyCta': 'Settings में खोलें',
'skills.tabs.composio': 'Composio',
'skills.tabs.channels': 'चैनल',
'skills.tabs.explorer': 'स्किल',
'skills.tabs.mcp': 'MCP सर्वर',
'memory.title': 'मेमोरी',
'memory.search': 'मेमोरी सर्च करें...',
@@ -4057,6 +4067,9 @@ const messages: TranslationMap = {
'skills.detail.closeAriaLabel': 'स्किल डिटेल्स बंद करें',
'skills.detail.location': 'लोकेशन',
'skills.detail.noBundledResources': 'कोई बंडल्ड रिसोर्स नहीं।',
'skills.detail.platforms': 'प्लैटफ़ॉर्म',
'skills.detail.relatedSkills': 'संबंधित स्किल',
'skills.detail.sourceFormat': 'फ़ॉर्मैट',
'skills.detail.tags': 'टैग्स',
'skills.detail.warnings': 'चेतावनियाँ',
'skills.install.errors.alreadyInstalledHint':
@@ -4161,6 +4174,13 @@ const messages: TranslationMap = {
'skills.resource.preview.closeAriaLabel': 'प्रीव्यू बंद करें',
'skills.resource.preview.failed': 'पूर्वावलोकन विफल',
'skills.resource.preview.loading': 'प्रीव्यू लोड हो रहा है…',
'skills.resource.tree.assets': 'एसेट',
'skills.resource.tree.examples': 'उदाहरण',
'skills.resource.tree.other': 'अन्य',
'skills.resource.tree.prompts': 'प्रॉम्प्ट',
'skills.resource.tree.references': 'संदर्भ',
'skills.resource.tree.scripts': 'स्क्रिप्ट',
'skills.resource.tree.templates': 'टेम्पलेट',
'skills.resource.tree.empty': 'कोई बंडल्ड रिसोर्स नहीं।',
'skills.search.placeholder': 'प्लेसहोल्डर',
'skills.setup.autocomplete.acceptKey': 'स्वीकार कुंजी',
+20
View File
@@ -258,6 +258,15 @@ const messages: TranslationMap = {
'skills.available': 'Tersedia',
'skills.addAccount': 'Tambah Akun',
'skills.channels': 'Kanal',
'skills.explorer.emptyCta': 'Instal dari URL',
'skills.explorer.emptyDescription':
'Instal paket SKILL.md atau letakkan folder bergaya Hermes di ~/.openhuman/skills.',
'skills.explorer.emptyTitle': 'Tidak ada skill ditemukan',
'skills.explorer.installFromUrl': 'Instal dari URL',
'skills.explorer.newSkill': 'Skill baru',
'skills.explorer.subtitle':
'Jelajahi paket SKILL.md, termasuk markdown, skrip, referensi, templat, contoh, dan prompt bergaya Hermes.',
'skills.explorer.title': 'Penjelajah skill',
'skills.integrations': 'Integrasi',
'skills.integrationsSubtitle':
'Koneksi OAuth berbasis cloud — masuk dengan akun Anda dan Composio mengelola token agar agen dapat membaca dan bertindak atas nama Anda. Tidak perlu mengelola API key.',
@@ -267,6 +276,7 @@ const messages: TranslationMap = {
'skills.composio.noApiKeyCta': 'Buka di Pengaturan',
'skills.tabs.composio': 'Composio',
'skills.tabs.channels': 'Saluran',
'skills.tabs.explorer': 'Skill',
'skills.tabs.mcp': 'MCP Server',
'memory.title': 'Memori',
'memory.search': 'Cari memori...',
@@ -4065,6 +4075,9 @@ const messages: TranslationMap = {
'skills.detail.closeAriaLabel': 'Tutup detail skill',
'skills.detail.location': 'Lokasi',
'skills.detail.noBundledResources': 'Tidak ada sumber daya bundel.',
'skills.detail.platforms': 'Platform',
'skills.detail.relatedSkills': 'Skill terkait',
'skills.detail.sourceFormat': 'Format',
'skills.detail.tags': 'Tag',
'skills.detail.warnings': 'Peringatan',
'skills.install.errors.alreadyInstalledHint':
@@ -4169,6 +4182,13 @@ const messages: TranslationMap = {
'skills.resource.preview.closeAriaLabel': 'Tutup pratinjau',
'skills.resource.preview.failed': 'Pratinjau gagal',
'skills.resource.preview.loading': 'Memuat pratinjau...',
'skills.resource.tree.assets': 'Aset',
'skills.resource.tree.examples': 'Contoh',
'skills.resource.tree.other': 'Lainnya',
'skills.resource.tree.prompts': 'Prompt',
'skills.resource.tree.references': 'Referensi',
'skills.resource.tree.scripts': 'Skrip',
'skills.resource.tree.templates': 'Templat',
'skills.resource.tree.empty': 'Tidak ada sumber daya bundel.',
'skills.search.placeholder': 'Teks placeholder',
'skills.setup.autocomplete.acceptKey': 'Kunci terima',
+20
View File
@@ -262,6 +262,15 @@ const messages: TranslationMap = {
'skills.available': 'Disponibile',
'skills.addAccount': 'Aggiungi account',
'skills.channels': 'Canali',
'skills.explorer.emptyCta': 'Installa da URL',
'skills.explorer.emptyDescription':
'Installa un pacchetto SKILL.md o inserisci cartelle in stile Hermes in ~/.openhuman/skills.',
'skills.explorer.emptyTitle': 'Nessuna skill trovata',
'skills.explorer.installFromUrl': 'Installa da URL',
'skills.explorer.newSkill': 'Nuova skill',
'skills.explorer.subtitle':
'Sfoglia pacchetti SKILL.md, inclusi markdown, script, riferimenti, modelli, esempi e prompt in stile Hermes.',
'skills.explorer.title': 'Esplora skill',
'skills.integrations': 'Integrazioni',
'skills.integrationsSubtitle':
'Connessioni OAuth basate su cloud — accedi con il tuo account e Composio gestisce i token affinché gli agenti possano leggere e agire per tuo conto. Nessuna chiave API da gestire.',
@@ -271,6 +280,7 @@ const messages: TranslationMap = {
'skills.composio.noApiKeyCta': 'Apri nelle impostazioni',
'skills.tabs.composio': 'Composio',
'skills.tabs.channels': 'Canali',
'skills.tabs.explorer': 'Skill',
'skills.tabs.mcp': 'MCP Server',
'memory.title': 'Memoria',
'memory.search': 'Cerca memorie...',
@@ -4119,6 +4129,9 @@ const messages: TranslationMap = {
'skills.detail.closeAriaLabel': 'Chiudi dettagli skill',
'skills.detail.location': 'Posizione',
'skills.detail.noBundledResources': 'Nessuna risorsa inclusa.',
'skills.detail.platforms': 'Piattaforme',
'skills.detail.relatedSkills': 'Skill correlate',
'skills.detail.sourceFormat': 'Formato',
'skills.detail.tags': 'Tag',
'skills.detail.warnings': 'Avvertenze',
'skills.install.errors.alreadyInstalledHint':
@@ -4223,6 +4236,13 @@ const messages: TranslationMap = {
'skills.resource.preview.closeAriaLabel': 'Chiudi anteprima',
'skills.resource.preview.failed': 'Anteprima fallita',
'skills.resource.preview.loading': 'Caricamento anteprima…',
'skills.resource.tree.assets': 'Risorse',
'skills.resource.tree.examples': 'Esempi',
'skills.resource.tree.other': 'Altro',
'skills.resource.tree.prompts': 'Prompt',
'skills.resource.tree.references': 'Riferimenti',
'skills.resource.tree.scripts': 'Script',
'skills.resource.tree.templates': 'Modelli',
'skills.resource.tree.empty': 'Nessuna risorsa inclusa.',
'skills.search.placeholder': 'Segnaposto',
'skills.setup.autocomplete.acceptKey': 'Tasto di accettazione',
+20
View File
@@ -256,6 +256,15 @@ const messages: TranslationMap = {
'skills.available': '사용 가능',
'skills.addAccount': '계정 추가',
'skills.channels': '채널',
'skills.explorer.emptyCta': 'URL에서 설치',
'skills.explorer.emptyDescription':
'SKILL.md 패키지를 설치하거나 Hermes 스타일 폴더를 ~/.openhuman/skills에 넣으세요.',
'skills.explorer.emptyTitle': '스킬을 찾을 수 없음',
'skills.explorer.installFromUrl': 'URL에서 설치',
'skills.explorer.newSkill': '새 스킬',
'skills.explorer.subtitle':
'Hermes 스타일 markdown, scripts, references, templates, examples, prompts를 포함한 SKILL.md 패키지를 탐색합니다.',
'skills.explorer.title': '스킬 탐색기',
'skills.integrations': '통합',
'skills.integrationsSubtitle':
'클라우드 기반 OAuth 연결 — 계정으로 로그인하면 Composio가 토큰을 관리하여 에이전트가 사용자를 대신해 읽고 작동할 수 있습니다. API 키 관리가 필요 없습니다.',
@@ -265,6 +274,7 @@ const messages: TranslationMap = {
'skills.composio.noApiKeyCta': '설정에서 열기',
'skills.tabs.composio': 'Composio',
'skills.tabs.channels': '채널',
'skills.tabs.explorer': '스킬',
'skills.tabs.mcp': 'MCP 서버',
'memory.title': '메모리',
'memory.search': '메모리 검색...',
@@ -4010,6 +4020,9 @@ const messages: TranslationMap = {
'skills.detail.closeAriaLabel': '스킬 세부 정보 닫기',
'skills.detail.location': '위치',
'skills.detail.noBundledResources': '번들 리소스가 없습니다.',
'skills.detail.platforms': '플랫폼',
'skills.detail.relatedSkills': '관련 스킬',
'skills.detail.sourceFormat': '형식',
'skills.detail.tags': '태그',
'skills.detail.warnings': '경고',
'skills.install.errors.alreadyInstalledHint':
@@ -4113,6 +4126,13 @@ const messages: TranslationMap = {
'skills.resource.preview.closeAriaLabel': '미리보기 닫기',
'skills.resource.preview.failed': '미리보기 실패',
'skills.resource.preview.loading': '미리보기 불러오는 중…',
'skills.resource.tree.assets': '자산',
'skills.resource.tree.examples': '예시',
'skills.resource.tree.other': '기타',
'skills.resource.tree.prompts': '프롬프트',
'skills.resource.tree.references': '참조',
'skills.resource.tree.scripts': '스크립트',
'skills.resource.tree.templates': '템플릿',
'skills.resource.tree.empty': '번들 리소스가 없습니다.',
'skills.search.placeholder': '플레이스홀더',
'skills.setup.autocomplete.acceptKey': '수락 키',
+20
View File
@@ -260,6 +260,15 @@ const messages: TranslationMap = {
'skills.available': 'Dostępne',
'skills.addAccount': 'Dodaj konto',
'skills.channels': 'Kanały',
'skills.explorer.emptyCta': 'Zainstaluj z URL',
'skills.explorer.emptyDescription':
'Zainstaluj pakiet SKILL.md albo umieść foldery w stylu Hermes w ~/.openhuman/skills.',
'skills.explorer.emptyTitle': 'Nie znaleziono skilli',
'skills.explorer.installFromUrl': 'Zainstaluj z URL',
'skills.explorer.newSkill': 'Nowy skill',
'skills.explorer.subtitle':
'Przeglądaj pakiety SKILL.md, w tym markdown, skrypty, referencje, szablony, przykłady i prompty w stylu Hermes.',
'skills.explorer.title': 'Eksplorator skilli',
'skills.integrations': 'Integracje Composio',
'skills.integrationsSubtitle':
'Chmurowe połączenia OAuth — zaloguj się swoim kontem, a Composio pośredniczy w tokenach, dzięki czemu agenci mogą czytać i działać w Twoim imieniu. Bez kluczy API do utrzymywania.',
@@ -269,6 +278,7 @@ const messages: TranslationMap = {
'skills.composio.noApiKeyCta': 'Otwórz ustawienia',
'skills.tabs.composio': 'Composio',
'skills.tabs.channels': 'Kanały',
'skills.tabs.explorer': 'Skille',
'skills.tabs.mcp': 'Serwery MCP',
'memory.title': 'Pamięć',
'memory.search': 'Szukaj w pamięci...',
@@ -4118,6 +4128,9 @@ const messages: TranslationMap = {
'skills.detail.closeAriaLabel': 'Zamknij szczegóły umiejętności',
'skills.detail.location': 'Lokalizacja',
'skills.detail.noBundledResources': 'Brak dołączonych zasobów.',
'skills.detail.platforms': 'Platformy',
'skills.detail.relatedSkills': 'Powiązane skille',
'skills.detail.sourceFormat': 'Format',
'skills.detail.tags': 'Tagi',
'skills.detail.warnings': 'Ostrzeżenia',
'skills.install.errors.alreadyInstalledHint':
@@ -4223,6 +4236,13 @@ const messages: TranslationMap = {
'skills.resource.preview.closeAriaLabel': 'Zamknij podgląd',
'skills.resource.preview.failed': 'Podgląd nie powiódł się',
'skills.resource.preview.loading': 'Wczytywanie podglądu…',
'skills.resource.tree.assets': 'Zasoby',
'skills.resource.tree.examples': 'Przykłady',
'skills.resource.tree.other': 'Inne',
'skills.resource.tree.prompts': 'Prompty',
'skills.resource.tree.references': 'Referencje',
'skills.resource.tree.scripts': 'Skrypty',
'skills.resource.tree.templates': 'Szablony',
'skills.resource.tree.empty': 'Brak dołączonych zasobów.',
'skills.search.placeholder': 'Szukaj umiejętności...',
'skills.setup.autocomplete.acceptKey': 'Klawisz akceptacji',
+20
View File
@@ -264,6 +264,15 @@ const messages: TranslationMap = {
'skills.available': 'Disponível',
'skills.addAccount': 'Adicionar Conta',
'skills.channels': 'Canais',
'skills.explorer.emptyCta': 'Instalar por URL',
'skills.explorer.emptyDescription':
'Instale um pacote SKILL.md ou coloque pastas no estilo Hermes em ~/.openhuman/skills.',
'skills.explorer.emptyTitle': 'Nenhuma skill encontrada',
'skills.explorer.installFromUrl': 'Instalar por URL',
'skills.explorer.newSkill': 'Nova skill',
'skills.explorer.subtitle':
'Explore pacotes SKILL.md, incluindo markdown, scripts, referências, modelos, exemplos e prompts no estilo Hermes.',
'skills.explorer.title': 'Explorador de skills',
'skills.integrations': 'Integrações',
'skills.integrationsSubtitle':
'Conexões OAuth baseadas em nuvem — faça login com sua conta e o Composio gerencia os tokens para que os agentes possam ler e agir em seu nome. Sem chaves de API para gerenciar.',
@@ -273,6 +282,7 @@ const messages: TranslationMap = {
'skills.composio.noApiKeyCta': 'Abrir nas Configurações',
'skills.tabs.composio': 'Composio',
'skills.tabs.channels': 'Canais',
'skills.tabs.explorer': 'Skills',
'skills.tabs.mcp': 'MCP Servidores',
'memory.title': 'Memória',
'memory.search': 'Pesquisar memórias...',
@@ -4120,6 +4130,9 @@ const messages: TranslationMap = {
'skills.detail.closeAriaLabel': 'Fechar detalhes da habilidade',
'skills.detail.location': 'Localização',
'skills.detail.noBundledResources': 'Sem recursos incluídos.',
'skills.detail.platforms': 'Plataformas',
'skills.detail.relatedSkills': 'Skills relacionadas',
'skills.detail.sourceFormat': 'Formato',
'skills.detail.tags': 'Tags',
'skills.detail.warnings': 'Avisos',
'skills.install.errors.alreadyInstalledHint':
@@ -4223,6 +4236,13 @@ const messages: TranslationMap = {
'skills.resource.preview.closeAriaLabel': 'Fechar visualização',
'skills.resource.preview.failed': 'Falha na pré-visualização',
'skills.resource.preview.loading': 'Carregando visualização…',
'skills.resource.tree.assets': 'Recursos',
'skills.resource.tree.examples': 'Exemplos',
'skills.resource.tree.other': 'Outros',
'skills.resource.tree.prompts': 'Prompts',
'skills.resource.tree.references': 'Referências',
'skills.resource.tree.scripts': 'Scripts',
'skills.resource.tree.templates': 'Modelos',
'skills.resource.tree.empty': 'Sem recursos incluídos.',
'skills.search.placeholder': 'Espaço reservado',
'skills.setup.autocomplete.acceptKey': 'Tecla de aceitar',
+20
View File
@@ -258,6 +258,15 @@ const messages: TranslationMap = {
'skills.available': 'Доступно',
'skills.addAccount': 'Добавить аккаунт',
'skills.channels': 'Каналы',
'skills.explorer.emptyCta': 'Установить по URL',
'skills.explorer.emptyDescription':
'Установите пакет SKILL.md или поместите папки в стиле Hermes в ~/.openhuman/skills.',
'skills.explorer.emptyTitle': 'Навыки не найдены',
'skills.explorer.installFromUrl': 'Установить по URL',
'skills.explorer.newSkill': 'Новый навык',
'skills.explorer.subtitle':
'Просматривайте пакеты SKILL.md, включая markdown, скрипты, справочные материалы, шаблоны, примеры и промпты в стиле Hermes.',
'skills.explorer.title': 'Обозреватель навыков',
'skills.integrations': 'Интеграции',
'skills.integrationsSubtitle':
'Облачные OAuth-подключения — войдите в свой аккаунт, и Composio управляет токенами, чтобы агенты могли читать и действовать от вашего имени. Никаких API-ключей для управления.',
@@ -267,6 +276,7 @@ const messages: TranslationMap = {
'skills.composio.noApiKeyCta': 'Открыть в настройках',
'skills.tabs.composio': 'Composio',
'skills.tabs.channels': 'Каналы',
'skills.tabs.explorer': 'Навыки',
'skills.tabs.mcp': 'MCP Серверы',
'memory.title': 'Память',
'memory.search': 'Поиск воспоминаний...',
@@ -4085,6 +4095,9 @@ const messages: TranslationMap = {
'skills.detail.closeAriaLabel': 'Закрыть детали навыка',
'skills.detail.location': 'Расположение',
'skills.detail.noBundledResources': 'Встроенных ресурсов нет.',
'skills.detail.platforms': 'Платформы',
'skills.detail.relatedSkills': 'Связанные навыки',
'skills.detail.sourceFormat': 'Формат',
'skills.detail.tags': 'Теги',
'skills.detail.warnings': 'Предупреждения',
'skills.install.errors.alreadyInstalledHint':
@@ -4187,6 +4200,13 @@ const messages: TranslationMap = {
'skills.resource.preview.closeAriaLabel': 'Закрыть предпросмотр',
'skills.resource.preview.failed': 'Не удалось показать превью',
'skills.resource.preview.loading': 'Загрузка предпросмотра…',
'skills.resource.tree.assets': 'Ресурсы',
'skills.resource.tree.examples': 'Примеры',
'skills.resource.tree.other': 'Другое',
'skills.resource.tree.prompts': 'Промпты',
'skills.resource.tree.references': 'Справка',
'skills.resource.tree.scripts': 'Скрипты',
'skills.resource.tree.templates': 'Шаблоны',
'skills.resource.tree.empty': 'Встроенных ресурсов нет.',
'skills.search.placeholder': 'Поиск',
'skills.setup.autocomplete.acceptKey': 'Клавиша принятия',
+20
View File
@@ -245,6 +245,15 @@ const messages: TranslationMap = {
'skills.available': '可用',
'skills.addAccount': '添加账户',
'skills.channels': '渠道',
'skills.explorer.emptyCta': '通过 URL 安装',
'skills.explorer.emptyDescription':
'安装 SKILL.md 包,或将 Hermes 风格的文件夹放到 ~/.openhuman/skills 下。',
'skills.explorer.emptyTitle': '未找到技能',
'skills.explorer.installFromUrl': '通过 URL 安装',
'skills.explorer.newSkill': '新建技能',
'skills.explorer.subtitle':
'浏览 SKILL.md 包,包括 Hermes 风格的 Markdown、脚本、参考资料、模板、示例和提示词。',
'skills.explorer.title': '技能浏览器',
'skills.integrations': '集成',
'skills.integrationsSubtitle':
'基于云端的 OAuth 连接——使用您的账户登录,Composio 代管令牌,让智能体能以您的名义读取数据并执行操作,无需管理 API 密钥。',
@@ -254,6 +263,7 @@ const messages: TranslationMap = {
'skills.composio.noApiKeyCta': '在设置中打开',
'skills.tabs.composio': 'Composio',
'skills.tabs.channels': '渠道',
'skills.tabs.explorer': '技能',
'skills.tabs.mcp': 'MCP 服务器',
'memory.title': '记忆',
'memory.search': '搜索记忆...',
@@ -3857,6 +3867,9 @@ const messages: TranslationMap = {
'skills.detail.closeAriaLabel': '关闭技能详情',
'skills.detail.location': '位置',
'skills.detail.noBundledResources': '无捆绑资源。',
'skills.detail.platforms': '平台',
'skills.detail.relatedSkills': '相关技能',
'skills.detail.sourceFormat': '格式',
'skills.detail.tags': '标签',
'skills.detail.warnings': '警告',
'skills.install.errors.alreadyInstalledHint':
@@ -3954,6 +3967,13 @@ const messages: TranslationMap = {
'skills.resource.preview.closeAriaLabel': '关闭预览',
'skills.resource.preview.failed': '预览失败',
'skills.resource.preview.loading': '加载预览中…',
'skills.resource.tree.assets': '资源',
'skills.resource.tree.examples': '示例',
'skills.resource.tree.other': '其他',
'skills.resource.tree.prompts': '提示词',
'skills.resource.tree.references': '参考资料',
'skills.resource.tree.scripts': '脚本',
'skills.resource.tree.templates': '模板',
'skills.resource.tree.empty': '无捆绑资源。',
'skills.search.placeholder': '搜索技能...',
'skills.setup.autocomplete.acceptKey': '接受键',
@@ -148,8 +148,9 @@ describe('Skills page — Channels grid', () => {
it('does not surface a Channels chip in the category filter inside the Integrations card', () => {
renderWithProviders(<Skills />, { initialEntries: ['/skills'] });
fireEvent.click(screen.getByRole('tab', { name: 'Composio' }));
// The composio tab is active by default — Composio Integrations card is visible.
// The Composio tab owns the Integrations category filter.
const integrationsHeading = screen.getByRole('heading', { name: 'Composio Integrations' });
const integrationsCard = integrationsHeading.closest('.rounded-2xl');
expect(integrationsCard).not.toBeNull();
@@ -73,8 +73,13 @@ describe('Skills page — Composio catalog fallback', () => {
agentReadyState = { agentReady: new Set<string>(), loading: true, error: null };
});
function openComposioTab() {
fireEvent.click(screen.getByRole('tab', { name: 'Composio' }));
}
it('shows known composio integrations in the integrations icon grid when the live toolkit list is empty', () => {
renderWithProviders(<Skills />, { initialEntries: ['/skills'] });
openComposioTab();
expect(screen.getByRole('heading', { name: 'Composio Integrations' })).toBeInTheDocument();
expect(screen.getByText('Discord')).toBeInTheDocument();
@@ -104,6 +109,7 @@ describe('Skills page — Composio catalog fallback', () => {
composioError = 'Backend unavailable';
renderWithProviders(<Skills />, { initialEntries: ['/skills'] });
openComposioTab();
expect(screen.getByText('Connections are showing stale status')).toBeInTheDocument();
expect(screen.getByText('Backend unavailable')).toBeInTheDocument();
@@ -129,6 +135,7 @@ describe('Skills page — Composio catalog fallback', () => {
]);
renderWithProviders(<Skills />, { initialEntries: ['/skills'] });
openComposioTab();
const integrationsSection = screen
.getByRole('heading', { name: 'Composio Integrations' })
@@ -157,6 +164,7 @@ describe('Skills page — Composio catalog fallback', () => {
agentReadyState = { agentReady: new Set<string>(), loading: false, error: 'rpc unavailable' };
renderWithProviders(<Skills />, { initialEntries: ['/skills'] });
openComposioTab();
const integrationsSection = screen
.getByRole('heading', { name: 'Composio Integrations' })
@@ -180,6 +188,7 @@ describe('Skills page — Composio catalog fallback', () => {
agentReadyState = { agentReady: new Set<string>(['gmail']), loading: false, error: null };
renderWithProviders(<Skills />, { initialEntries: ['/skills'] });
openComposioTab();
const integrationsSection = screen
.getByRole('heading', { name: 'Composio Integrations' })
@@ -204,6 +213,7 @@ describe('Skills page — Composio catalog fallback', () => {
composioModeStatus = { result: { mode: 'direct', api_key_set: false }, logs: [] };
renderWithProviders(<Skills />, { initialEntries: ['/skills'] });
openComposioTab();
await waitFor(() => {
expect(screen.getByText(/No Composio API Key Configured/i)).toBeInTheDocument();
@@ -38,6 +38,7 @@ vi.mock('../../lib/composio/hooks', () => ({
describe('Skills page — Gmail composio integration', () => {
it('renders Gmail as a connected composio integration and opens its management modal', async () => {
renderWithProviders(<Skills />, { initialEntries: ['/skills'] });
fireEvent.click(screen.getByRole('tab', { name: 'Composio' }));
const integrationsSection = screen
.getByRole('heading', { name: 'Composio Integrations' })
@@ -36,6 +36,7 @@ vi.mock('../../lib/composio/hooks', () => ({
describe('Skills page — Notion composio integration', () => {
it('renders Notion as a disconnected composio integration and opens its connect modal', async () => {
renderWithProviders(<Skills />, { initialEntries: ['/skills'] });
fireEvent.click(screen.getByRole('tab', { name: 'Composio' }));
expect(screen.getByRole('heading', { name: 'Composio Integrations' })).toBeInTheDocument();
const notionTile = screen.getByRole('button', { name: /Notion.*Connect/i });
@@ -4,6 +4,61 @@ import { skillsApi } from '../skillsApi';
vi.mock('../../coreRpcClient', () => ({ callCoreRpc: vi.fn() }));
describe('skillsApi.listSkills', () => {
beforeEach(async () => {
const { callCoreRpc } = await import('../../coreRpcClient');
vi.mocked(callCoreRpc).mockReset();
});
it('normalizes snake_case and legacy discovered skill fields', async () => {
const { callCoreRpc } = await import('../../coreRpcClient');
vi.mocked(callCoreRpc).mockResolvedValueOnce({
skills: [
{
id: 'hermes-demo',
name: 'Hermes Demo',
description: 'Reads Hermes metadata.',
version: '',
author: null,
tags: [],
related_skills: ['browser-automation'],
source_format: 'hermes',
tools: [],
prompts: [],
location: null,
resources: [],
scope: 'user',
legacy: false,
warnings: [],
},
{
id: 'legacy-demo',
name: 'Legacy Demo',
description: 'Old package.',
version: '',
author: null,
tags: [],
tools: [],
prompts: [],
location: null,
resources: [],
scope: 'user',
legacy: true,
warnings: [],
},
],
});
const result = await skillsApi.listSkills();
expect(callCoreRpc).toHaveBeenCalledWith({ method: 'openhuman.workflows_list' });
expect(result[0].relatedSkills).toEqual(['browser-automation']);
expect(result[0].sourceFormat).toBe('hermes');
expect(result[0].platforms).toEqual([]);
expect(result[1].sourceFormat).toBe('legacy');
});
});
describe('skillsApi.createSkill', () => {
beforeEach(async () => {
const { callCoreRpc } = await import('../../coreRpcClient');
+32 -7
View File
@@ -32,6 +32,12 @@ export interface SkillSummary {
author: string | null;
/** Tags declared in frontmatter metadata. */
tags: string[];
/** Platform compatibility hints from SKILL.md frontmatter. */
platforms: string[];
/** Related skills declared by the originating ecosystem. */
relatedSkills: string[];
/** Normalized source format hint, e.g. openhuman, hermes, legacy. */
sourceFormat: string;
/** Tool hint from `allowed-tools`. */
tools: string[];
/** Prompt files declared in the legacy manifest. */
@@ -49,9 +55,17 @@ export interface SkillSummary {
}
interface SkillsListResult {
skills: SkillSummary[];
skills: RawSkillSummary[];
}
type RawSkillSummary = Omit<SkillSummary, 'platforms' | 'relatedSkills' | 'sourceFormat'> & {
platforms?: string[];
related_skills?: string[];
relatedSkills?: string[];
source_format?: string;
sourceFormat?: string;
};
/**
* Result of `openhuman.workflows_read_resource`.
*/
@@ -120,7 +134,7 @@ export interface CreateSkillInput {
}
interface RawSkillsCreateResult {
skill: SkillSummary;
skill: RawSkillSummary;
}
/**
@@ -190,6 +204,15 @@ function unwrapEnvelope<T>(response: Envelope<T> | T): T {
return response as T;
}
function normalizeSkillSummary(raw: RawSkillSummary): SkillSummary {
return {
...raw,
platforms: raw.platforms ?? [],
relatedSkills: raw.relatedSkills ?? raw.related_skills ?? [],
sourceFormat: raw.sourceFormat ?? raw.source_format ?? (raw.legacy ? 'legacy' : 'openhuman'),
};
}
export const skillsApi = {
/** Enumerate SKILL.md / legacy skills visible in the active workspace. */
listSkills: async (): Promise<SkillSummary[]> => {
@@ -198,7 +221,7 @@ export const skillsApi = {
method: 'openhuman.workflows_list',
});
const result = unwrapEnvelope(response);
const skills = result?.skills ?? [];
const skills = (result?.skills ?? []).map(normalizeSkillSummary);
log('listSkills: response count=%d', skills.length);
return skills;
},
@@ -259,8 +282,9 @@ export const skillsApi = {
},
});
const raw = unwrapEnvelope(response);
log('createSkill: response id=%s', raw.skill.id);
return raw.skill;
const skill = normalizeSkillSummary(raw.skill);
log('createSkill: response id=%s', skill.id);
return skill;
},
/**
@@ -288,8 +312,9 @@ export const skillsApi = {
},
});
const raw = unwrapEnvelope(response);
log('updateSkill: response id=%s', raw.skill.id);
return raw.skill;
const skill = normalizeSkillSummary(raw.skill);
log('updateSkill: response id=%s', skill.id);
return skill;
},
/**
@@ -81,6 +81,7 @@ async function bootSkillsPage(page: Page, userId: string) {
.toContain('/skills');
await waitForAppReady(page);
await dismissWalkthroughIfPresent(page);
await page.getByRole('tab', { name: 'Composio' }).click();
const heading = page.getByRole('heading', { name: 'Composio Integrations' });
if (!(await heading.isVisible().catch(() => false))) {
const connectionsButton = page.getByRole('button', { name: 'Connections' });
@@ -189,6 +190,7 @@ test.describe('Composio triggers flow', () => {
await page.reload();
await waitForAppReady(page);
await dismissWalkthroughIfPresent(page);
await page.getByRole('tab', { name: 'Composio' }).click();
await expect(page.getByRole('heading', { name: 'Composio Integrations' })).toBeVisible({
timeout: 20_000,
});
@@ -74,6 +74,7 @@ async function bootSkillsPage(page: Page, userId: string) {
.toContain('/skills');
await waitForAppReady(page);
await dismissWalkthroughIfPresent(page);
await page.getByRole('tab', { name: 'Composio' }).click();
const heading = page.getByRole('heading', { name: 'Composio Integrations' });
if (!(await heading.isVisible().catch(() => false))) {
const connectionsButton = page.getByRole('button', { name: 'Connections' });
@@ -106,6 +107,7 @@ async function ensureComposioSurface(page: Page) {
.toContain('/skills');
await waitForAppReady(page);
await dismissWalkthroughIfPresent(page);
await page.getByRole('tab', { name: 'Composio' }).click();
if (await heading.isVisible().catch(() => false)) {
return;
}
@@ -114,6 +116,7 @@ async function ensureComposioSurface(page: Page) {
await connectionsButton.click({ force: true });
await waitForAppReady(page);
await dismissWalkthroughIfPresent(page);
await page.getByRole('tab', { name: 'Composio' }).click();
if (await heading.isVisible().catch(() => false)) {
return;
}
@@ -72,6 +72,7 @@ async function bootSkills(page: Page, userId: string): Promise<void> {
await page.goto('/#/skills');
await waitForAppReady(page);
await dismissWalkthroughIfPresent(page);
await page.getByRole('tab', { name: 'Composio' }).click();
await expect(page.getByRole('heading', { name: 'Composio Integrations' })).toBeVisible({
timeout: 20_000,
});
@@ -142,6 +143,7 @@ test.describe('Connector session guard matrix', () => {
});
await page.reload();
await waitForAppReady(page);
await page.getByRole('tab', { name: 'Composio' }).click();
await page.getByTestId('skill-install-composio-jira').click();
const dialog = page.getByRole('dialog', { name: /Jira/i });
await expect(dialog).toBeVisible();
@@ -154,12 +156,14 @@ test.describe('Connector session guard matrix', () => {
await seedToolkits('FAILED');
await page.reload();
await waitForAppReady(page);
await page.getByRole('tab', { name: 'Composio' }).click();
await expect(page.getByTestId('skill-install-composio-discord')).toContainText('Discord');
await assertSessionAlive(page);
await seedToolkits('EXPIRED');
await page.reload();
await waitForAppReady(page);
await page.getByRole('tab', { name: 'Composio' }).click();
await expect(page.getByTestId('skill-install-composio-github')).toContainText(
/Reconnect|GitHub/
);
@@ -69,6 +69,7 @@ async function bootSkillsPage(page: Page, userId: string) {
});
await waitForAppReady(page);
await dismissWalkthroughIfPresent(page);
await page.getByRole('tab', { name: 'Composio' }).click();
const heading = page.getByRole('heading', { name: 'Composio Integrations' });
if (!(await heading.isVisible().catch(() => false))) {
const connectionsButton = page.getByRole('button', { name: 'Connections' });
@@ -102,6 +103,7 @@ test.describe('Gmail Integration Flows', () => {
await page.reload();
await waitForAppReady(page);
await dismissWalkthroughIfPresent(page);
await page.getByRole('tab', { name: 'Composio' }).click();
await page.getByTestId('skill-install-composio-gmail').click();
await expect(page.getByRole('dialog', { name: /Connect Gmail/i })).toBeVisible();
@@ -127,11 +129,13 @@ test.describe('Gmail Integration Flows', () => {
await seedConnector('FAILED');
await page.reload();
await waitForAppReady(page);
await page.getByRole('tab', { name: 'Composio' }).click();
await expect(page.getByTestId('skill-install-composio-gmail')).toContainText(CONNECTOR_NAME);
await seedConnector('EXPIRED');
await page.reload();
await waitForAppReady(page);
await page.getByRole('tab', { name: 'Composio' }).click();
await expect(page.getByTestId(`skill-install-composio-${TOOLKIT_SLUG}`)).toContainText(
/Auth expired|Reconnect/i
);
@@ -139,6 +143,7 @@ test.describe('Gmail Integration Flows', () => {
test('execute and disconnect routes do not blank the skills page', async ({ page }) => {
await callCoreRpc('openhuman.composio_execute', { tool: ACTION, arguments: {} });
await page.getByRole('tab', { name: 'Composio' }).click();
await expect(page.getByRole('heading', { name: 'Composio Integrations' })).toBeVisible();
await callCoreRpc('openhuman.composio_delete_connection', { connection_id: CONNECTION_ID });
@@ -44,6 +44,14 @@ test.describe('Intelligence memory UI', () => {
const label = `Playwright Memory Source ${Date.now()}`;
await openMemory(page);
await addFolderSource(label);
await page.reload();
await waitForAppReady(page);
await dismissWalkthroughIfPresent(page);
const memoryTab = page.getByRole('tab', { name: /^Memory$/ });
if (await memoryTab.isVisible().catch(() => false)) {
await memoryTab.click();
}
await expect(page.getByTestId('memory-workspace')).toBeVisible({ timeout: 20_000 });
const row = page.getByTestId('memory-source-row-folder').filter({ hasText: label });
await expect(row).toBeVisible({ timeout: 20_000 });
@@ -34,7 +34,11 @@ test.describe('Skills registry flow', () => {
await expect(page.getByRole('tab', { name: 'Composio' })).toBeVisible();
await expect(page.getByRole('tab', { name: 'Channels' })).toBeVisible();
await expect(page.getByRole('tab', { name: 'MCP Servers' })).toBeVisible();
await page.getByRole('tab', { name: 'Composio' }).click();
await expect(page.getByRole('heading', { name: 'Composio Integrations' })).toBeVisible();
await expect(
page.getByText(/Gmail|Notion|Telegram|GitHub|Google Drive/, { exact: false }).first()
).toBeVisible();
});
test('shows at least one known Composio integration name', async ({ page }) => {
+3
View File
@@ -266,6 +266,9 @@ mod tests {
version: "1.0.0".into(),
author: None,
tags: vec![],
platforms: vec![],
related_skills: vec![],
source_format: "openhuman".to_string(),
tools: vec![],
prompts: vec![],
location,
+5 -2
View File
@@ -384,7 +384,7 @@ mod tests {
store.clear().await;
let response = start(
MonitorStartRequest {
command: "printf '%1049600s\\n' x".into(),
command: "head -c 1200000 /dev/zero | tr '\\0' x".into(),
description: None,
timeout_ms: Some(2_000),
persistent: false,
@@ -413,7 +413,10 @@ mod tests {
assert!(snapshot.output_bytes <= MAX_OUTPUT_BYTES);
assert!(
snapshot.dropped_bytes > 0,
"large monitor output should be dropped after the bound"
"large monitor output should be dropped after the bound (output_bytes={}, dropped_bytes={}, status={:?})",
snapshot.output_bytes,
snapshot.dropped_bytes,
snapshot.status
);
}
}
+3
View File
@@ -510,6 +510,9 @@ mod tests {
version: "0.1.0".into(),
author: None,
tags: Vec::new(),
platforms: Vec::new(),
related_skills: Vec::new(),
source_format: "openhuman".to_string(),
tools: Vec::new(),
prompts: Vec::new(),
location: None,
+75 -11
View File
@@ -10,6 +10,23 @@ use super::ops_types::{
WORKFLOW_MD,
};
const EXCLUDED_SKILL_DIRS: &[&str] = &[
".git",
".github",
".hub",
".archive",
".venv",
"venv",
"node_modules",
"site-packages",
"__pycache__",
".tox",
".nox",
".pytest_cache",
".mypy_cache",
".ruff_cache",
];
/// Initialize the legacy skills directory in the specified workspace.
///
/// Creates `<workspace>/skills/` and a placeholder `README.md` so the folder
@@ -179,9 +196,16 @@ fn precedence(scope: WorkflowScope) -> u8 {
}
fn scan_root(root: &Path, scope: WorkflowScope) -> Vec<Workflow> {
let mut out = Vec::new();
scan_root_inner(root, scope, &mut out);
out.sort_by(|a, b| a.dir_name.cmp(&b.dir_name));
out
}
fn scan_root_inner(root: &Path, scope: WorkflowScope, out: &mut Vec<Workflow>) {
let entries = match std::fs::read_dir(root) {
Ok(entries) => entries,
Err(_) => return Vec::new(),
Err(_) => return,
};
// `read_dir` order is unspecified. When two sibling directories declare
@@ -192,7 +216,6 @@ fn scan_root(root: &Path, scope: WorkflowScope) -> Vec<Workflow> {
let mut entries: Vec<_> = entries.flatten().collect();
entries.sort_by_key(|entry| entry.file_name());
let mut out = Vec::new();
for entry in entries {
// Use `file_type()` rather than `path.is_dir()` so a symlinked
// child cannot be loaded as a skill. `is_dir()` dereferences
@@ -209,14 +232,15 @@ fn scan_root(root: &Path, scope: WorkflowScope) -> Vec<Workflow> {
}
let path = entry.path();
let dir_name = entry.file_name().to_string_lossy().to_string();
if dir_name.starts_with('.') {
if dir_name.starts_with('.') || EXCLUDED_SKILL_DIRS.contains(&dir_name.as_str()) {
continue;
}
if let Some(skill) = load_skill_dir(&path, &dir_name, scope) {
out.push(skill);
continue;
}
scan_root_inner(&path, scope, out);
}
out
}
fn load_skill_dir(dir: &Path, dir_name: &str, scope: WorkflowScope) -> Option<Workflow> {
@@ -257,8 +281,9 @@ fn load_skill_dir(dir: &Path, dir_name: &str, scope: WorkflowScope) -> Option<Wo
/// Read a bundled skill resource as UTF-8 text, hardened against directory
/// traversal, symlink escape, and oversized payloads.
///
/// `skill_id` identifies the skill by its discovered `name` — the same field
/// surfaced on [`Workflow::name`]. The skill is resolved by running the standard
/// `skill_id` identifies the skill by its discovered `name` or its on-disk
/// `dir_name` slug — the same identifiers surfaced in the UI summary. The
/// skill is resolved by running the standard
/// discovery pipeline (`dirs::home_dir()` + `workspace_dir`, honoring the
/// `.openhuman/trust` marker) and locating the matching entry; this keeps the
/// read scoped to legitimately installed skills and reuses all the symlink /
@@ -320,11 +345,7 @@ pub fn read_workflow_resource(
// `load_workflow_metadata` (which honors both user and workspace roots plus the
// trust marker) so the resource read is scoped to the exact same set of
// skills the UI would already have shown the user.
let skills = load_workflow_metadata(workspace_dir);
let skill = skills
.into_iter()
.find(|s| s.name == skill_id)
.ok_or_else(|| format!("skill '{skill_id}' not found"))?;
let skill = resolve_workflow_for_resource(load_workflow_metadata(workspace_dir), skill_id)?;
let skill_root = skill
.location
.as_deref()
@@ -400,3 +421,46 @@ pub fn read_workflow_resource(
Ok(content)
}
fn resolve_workflow_for_resource(
workflows: Vec<Workflow>,
skill_id: &str,
) -> Result<Workflow, String> {
let mut dir_match: Option<Workflow> = None;
let mut name_match: Option<Workflow> = None;
for workflow in workflows {
if workflow.dir_name == skill_id {
if dir_match.is_some() {
return Err(format!(
"skill id '{skill_id}' is ambiguous across multiple skill directories"
));
}
dir_match = Some(workflow);
continue;
}
if workflow.name == skill_id {
if name_match.is_some() {
return Err(format!(
"skill name '{skill_id}' is ambiguous; use the directory id"
));
}
name_match = Some(workflow);
}
}
match (dir_match, name_match) {
(Some(dir_skill), Some(name_skill)) => {
if dir_skill.location == name_skill.location {
Ok(dir_skill)
} else {
Err(format!(
"skill id '{skill_id}' matches both a directory id and a different skill name"
))
}
}
(Some(skill), None) | (None, Some(skill)) => Ok(skill),
(None, None) => Err(format!("skill '{skill_id}' not found")),
}
}
+12 -1
View File
@@ -2,7 +2,9 @@
use std::path::{Path, PathBuf};
use super::ops_types::{extract_author, extract_tags, extract_version};
use super::ops_types::{
detect_source_format, extract_author, extract_related_skills, extract_tags, extract_version,
};
use super::ops_types::{
LegacyWorkflowManifest, Workflow, WorkflowFrontmatter, WorkflowScope, MAX_DESCRIPTION_LEN,
MAX_NAME_LEN, RESOURCE_DIRS,
@@ -199,6 +201,9 @@ pub(crate) fn load_from_workflow_md(
let version = extract_version(&frontmatter, &mut warnings);
let author = extract_author(&frontmatter, &mut warnings);
let tags = extract_tags(&frontmatter, &mut warnings);
let platforms = frontmatter.platforms.clone();
let related_skills = extract_related_skills(&frontmatter);
let source_format = detect_source_format(&frontmatter);
let tools = frontmatter.allowed_tools.clone();
Workflow {
@@ -208,6 +213,9 @@ pub(crate) fn load_from_workflow_md(
version,
author,
tags,
platforms,
related_skills,
source_format,
tools,
prompts: Vec::new(),
location: Some(skill_md.to_path_buf()),
@@ -272,6 +280,9 @@ pub(crate) fn load_from_legacy_manifest(
version: manifest.version,
author: manifest.author,
tags: manifest.tags,
platforms: Vec::new(),
related_skills: Vec::new(),
source_format: "legacy".to_string(),
tools: manifest.tools,
prompts: manifest.prompts,
location,
+97
View File
@@ -233,6 +233,60 @@ fn inventory_resources_lists_scripts_and_assets() {
assert!(!res.iter().any(|p| p.ends_with("x.txt")));
}
#[test]
fn inventory_resources_lists_hermes_resource_dirs() {
let dir = tempfile::tempdir().unwrap();
let skill = dir.path().join("s");
write(
&skill.join("SKILL.md"),
"---\nname: s\ndescription: d\n---\n",
);
write(&skill.join("templates").join("page.html"), "<html></html>");
write(&skill.join("examples").join("demo.md"), "demo");
write(&skill.join("prompts").join("system.md"), "prompt");
let mut res = inventory_resources(&skill);
res.sort();
assert_eq!(res.len(), 3);
assert!(res.iter().any(|p| p.ends_with("page.html")));
assert!(res.iter().any(|p| p.ends_with("demo.md")));
assert!(res.iter().any(|p| p.ends_with("system.md")));
}
#[test]
fn nested_hermes_skill_tree_discovers_metadata_and_resources() {
let dir = tempfile::tempdir().unwrap();
let ws = dir.path();
write(&ws.join(".openhuman").join("trust"), "");
let skill_dir = ws
.join(".openhuman")
.join("skills")
.join("creative")
.join("concept-diagrams");
write(
&skill_dir.join("SKILL.md"),
"---\nname: concept-diagrams\ndescription: Generate diagrams\nversion: 0.1.0\nauthor: Nous\nplatforms: [linux, macos, windows]\nmetadata:\n hermes:\n tags: [diagrams, svg]\n related_skills: [architecture-diagram]\n---\n",
);
write(
&skill_dir.join("templates").join("template.html"),
"<html></html>",
);
write(&skill_dir.join("examples").join("flow.md"), "flow");
let skills = load_skills_ws(ws);
assert_eq!(skills.len(), 1);
let s = &skills[0];
assert_eq!(s.name, "concept-diagrams");
assert_eq!(s.version, "0.1.0");
assert_eq!(s.author.as_deref(), Some("Nous"));
assert_eq!(s.platforms, vec!["linux", "macos", "windows"]);
assert_eq!(s.tags, vec!["diagrams", "svg"]);
assert_eq!(s.related_skills, vec!["architecture-diagram"]);
assert_eq!(s.source_format, "hermes");
assert!(s.resources.iter().any(|p| p.ends_with("template.html")));
assert!(s.resources.iter().any(|p| p.ends_with("flow.md")));
}
#[test]
fn parse_skill_md_without_frontmatter_returns_body() {
let dir = tempfile::tempdir().unwrap();
@@ -384,6 +438,49 @@ fn read_skill_resource_happy_path() {
assert_eq!(got, "#!/bin/sh\necho hi\n");
}
#[test]
fn read_skill_resource_uses_directory_id_when_display_name_differs() {
let dir = tempfile::tempdir().unwrap();
let ws = dir.path();
let skill_dir = ws.join("skills").join("demo-slug");
write(
&skill_dir.join("SKILL.md"),
"---\nname: Demo Display\ndescription: test skill\n---\n",
);
write(&skill_dir.join("references").join("note.md"), "slug read");
let got = read_workflow_resource(ws, "demo-slug", Path::new("references/note.md"))
.expect("directory id should resolve");
assert_eq!(got, "slug read");
}
#[test]
fn read_skill_resource_rejects_directory_name_collision() {
let dir = tempfile::tempdir().unwrap();
let ws = dir.path();
let named_demo = ws.join("skills").join("alpha");
write(
&named_demo.join("SKILL.md"),
"---\nname: demo\ndescription: display-name collision\n---\n",
);
write(&named_demo.join("references").join("note.md"), "alpha");
let slug_demo = ws.join("skills").join("demo");
write(
&slug_demo.join("SKILL.md"),
"---\nname: slug-demo\ndescription: slug collision\n---\n",
);
write(&slug_demo.join("references").join("note.md"), "demo");
let err = read_workflow_resource(ws, "demo", Path::new("references/note.md"))
.expect_err("ambiguous directory/name collision must be rejected");
assert!(
err.to_lowercase().contains("matches both"),
"unexpected error: {err}"
);
}
#[test]
fn read_skill_resource_rejects_parent_dir_traversal() {
let dir = tempfile::tempdir().unwrap();
+65 -4
View File
@@ -17,7 +17,17 @@ pub(crate) const SKILL_TOML: &str = "skill.toml";
pub(crate) const SKILL_JSON: &str = "skill.json";
pub(crate) const MAX_NAME_LEN: usize = 64;
pub(crate) const MAX_DESCRIPTION_LEN: usize = 1024;
pub(crate) const RESOURCE_DIRS: &[&str] = &["scripts", "references", "assets"];
pub(crate) const RESOURCE_DIRS: &[&str] = &[
"scripts",
"references",
"assets",
// Hermes-style skills commonly ship extra markdown/code under these
// directories. Treat them as browsable resources, not executable runtime
// entrypoints.
"templates",
"examples",
"prompts",
];
/// Upper bound on resource payload size (in bytes) returned by
/// [`read_workflow_resource`]. 128 KB is large enough for a typical SKILL-bundled
@@ -66,6 +76,10 @@ pub struct WorkflowFrontmatter {
pub license: Option<String>,
#[serde(default)]
pub compatibility: Option<String>,
/// Platform compatibility hint used by Hermes-style skills. Absence means
/// all platforms.
#[serde(default)]
pub platforms: Vec<String>,
/// Spec-compliant metadata map. Version, author, tags, and other
/// non-required fields live here.
#[serde(default)]
@@ -139,15 +153,53 @@ pub(crate) fn extract_author(
}
pub(crate) fn extract_tags(fm: &WorkflowFrontmatter, warnings: &mut Vec<String>) -> Vec<String> {
let mut tags = Vec::new();
if let Some(v) = fm.metadata.get("tags") {
return metadata_string_seq(v);
tags.extend(metadata_string_seq(v));
}
if let Some(hermes_tags) = fm
.metadata
.get("hermes")
.and_then(|v| v.as_mapping())
.and_then(|m| m.get(&serde_yaml::Value::String("tags".to_string())))
{
tags.extend(metadata_string_seq(hermes_tags));
}
if let Some(v) = fm.extra.get("tags") {
log::warn!("[skills] top-level 'tags' is deprecated; move under 'metadata.tags'");
warnings.push("top-level 'tags' is deprecated; move under 'metadata.tags'".to_string());
return metadata_string_seq(v);
tags.extend(metadata_string_seq(v));
}
tags.sort();
tags.dedup();
tags
}
pub(crate) fn extract_related_skills(fm: &WorkflowFrontmatter) -> Vec<String> {
let mut related = fm
.metadata
.get("related_skills")
.map(metadata_string_seq)
.unwrap_or_default();
if let Some(hermes_related) = fm
.metadata
.get("hermes")
.and_then(|v| v.as_mapping())
.and_then(|m| m.get(&serde_yaml::Value::String("related_skills".to_string())))
{
related.extend(metadata_string_seq(hermes_related));
}
related.sort();
related.dedup();
related
}
pub(crate) fn detect_source_format(fm: &WorkflowFrontmatter) -> String {
if fm.metadata.contains_key("hermes") || !fm.platforms.is_empty() {
"hermes".to_string()
} else {
"openhuman".to_string()
}
Vec::new()
}
/// A discovered skill.
@@ -169,6 +221,15 @@ pub struct Workflow {
pub author: Option<String>,
/// Tags declared in frontmatter.
pub tags: Vec<String>,
/// Compatible platform hints from SKILL.md frontmatter.
#[serde(default)]
pub platforms: Vec<String>,
/// Related skill names declared by the ecosystem/catalog.
#[serde(default)]
pub related_skills: Vec<String>,
/// Normalized ecosystem format hint (`openhuman`, `hermes`, or `legacy`).
#[serde(default)]
pub source_format: String,
/// Tool hint declared in frontmatter (`allowed-tools`).
#[serde(default)]
pub tools: Vec<String>,
+14
View File
@@ -111,6 +111,9 @@ struct WorkflowSummary {
version: String,
author: Option<String>,
tags: Vec<String>,
platforms: Vec<String>,
related_skills: Vec<String>,
source_format: String,
tools: Vec<String>,
prompts: Vec<String>,
location: Option<String>,
@@ -138,6 +141,17 @@ impl From<Workflow> for WorkflowSummary {
version: s.version,
author: s.author,
tags: s.tags,
platforms: s.platforms,
related_skills: s.related_skills,
source_format: if s.source_format.is_empty() {
if s.legacy {
"legacy".to_string()
} else {
"openhuman".to_string()
}
} else {
s.source_format
},
tools: s.tools,
prompts: s.prompts,
location: s.location.as_ref().map(|p| p.display().to_string()),