diff --git a/app/src/components/intelligence/__tests__/WorkflowsTab.test.tsx b/app/src/components/intelligence/__tests__/WorkflowsTab.test.tsx index c0dfe450f..42668d163 100644 --- a/app/src/components/intelligence/__tests__/WorkflowsTab.test.tsx +++ b/app/src/components/intelligence/__tests__/WorkflowsTab.test.tsx @@ -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 => ({ version: '0.1.0', author: null, tags: [], + platforms: [], + relatedSkills: [], + sourceFormat: 'openhuman', tools: [], prompts: [], location: null, diff --git a/app/src/components/skills/SkillResourceTree.tsx b/app/src/components/skills/SkillResourceTree.tsx index 567e28b5d..a8be61344 100644 --- a/app/src/components/skills/SkillResourceTree.tsx +++ b/app/src/components/skills/SkillResourceTree.tsx @@ -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 = { 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', }; diff --git a/app/src/components/skills/__tests__/CreateSkillModal.test.tsx b/app/src/components/skills/__tests__/CreateSkillModal.test.tsx index 57dcc13cd..0a4f98be9 100644 --- a/app/src/components/skills/__tests__/CreateSkillModal.test.tsx +++ b/app/src/components/skills/__tests__/CreateSkillModal.test.tsx @@ -33,6 +33,9 @@ function builtSkill(overrides: Partial = {}): SkillSummary { version: '', author: null, tags: [], + platforms: [], + relatedSkills: [], + sourceFormat: 'openhuman', tools: [], prompts: [], location: '/home/u/.openhuman/skills/my-skill/SKILL.md', diff --git a/app/src/components/skills/__tests__/CreateWorkflowForm.test.tsx b/app/src/components/skills/__tests__/CreateWorkflowForm.test.tsx index d75c4fa40..7a24e714c 100644 --- a/app/src/components/skills/__tests__/CreateWorkflowForm.test.tsx +++ b/app/src/components/skills/__tests__/CreateWorkflowForm.test.tsx @@ -372,6 +372,9 @@ describe('CreateWorkflowForm — edit mode', () => { version: '', author: 'me', tags: ['t1'], + platforms: [], + relatedSkills: [], + sourceFormat: 'openhuman', tools: ['shell'], prompts: [], location: null, diff --git a/app/src/components/skills/__tests__/UninstallSkillConfirmDialog.test.tsx b/app/src/components/skills/__tests__/UninstallSkillConfirmDialog.test.tsx index 42b6cedcb..e6c17f8d4 100644 --- a/app/src/components/skills/__tests__/UninstallSkillConfirmDialog.test.tsx +++ b/app/src/components/skills/__tests__/UninstallSkillConfirmDialog.test.tsx @@ -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', diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index 3969e38e4..5f8d622ae 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -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': 'مفتاح القبول', diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index 27f955a19..c6266dc76 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -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': 'অ্যাক্সেপ্ট কী', diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index d19af5b5d..a8ebccfeb 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -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', diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index a6d6ffb84..0d62339ea 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -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.', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index eb97517a6..cdf6a62a8 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -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', diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index 0c8c9ba20..eab483af6 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -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", diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index b836c670c..3b1421508 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -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': 'स्वीकार कुंजी', diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index e85f0c89f..3cee150cb 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -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', diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index 62500e428..d1b982189 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -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', diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index 02ff6e09c..407e06db2 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -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': '수락 키', diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index 952ed3bc7..bcbe48181 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -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', diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index 9bf62b81c..ee2d6b6bd 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -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', diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index 1c51268e2..df32dbabf 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -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': 'Клавиша принятия', diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index f4f0229f3..81c2affaf 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -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': '接受键', diff --git a/app/src/pages/__tests__/Skills.channels-grid.test.tsx b/app/src/pages/__tests__/Skills.channels-grid.test.tsx index f06d11dd6..c48973b90 100644 --- a/app/src/pages/__tests__/Skills.channels-grid.test.tsx +++ b/app/src/pages/__tests__/Skills.channels-grid.test.tsx @@ -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(, { 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(); diff --git a/app/src/pages/__tests__/Skills.composio-catalog.test.tsx b/app/src/pages/__tests__/Skills.composio-catalog.test.tsx index de612f978..913620c5f 100644 --- a/app/src/pages/__tests__/Skills.composio-catalog.test.tsx +++ b/app/src/pages/__tests__/Skills.composio-catalog.test.tsx @@ -73,8 +73,13 @@ describe('Skills page — Composio catalog fallback', () => { agentReadyState = { agentReady: new Set(), 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(, { 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(, { 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(, { 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(), loading: false, error: 'rpc unavailable' }; renderWithProviders(, { 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(['gmail']), loading: false, error: null }; renderWithProviders(, { 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(, { initialEntries: ['/skills'] }); + openComposioTab(); await waitFor(() => { expect(screen.getByText(/No Composio API Key Configured/i)).toBeInTheDocument(); diff --git a/app/src/pages/__tests__/Skills.third-party-gmail-sync.test.tsx b/app/src/pages/__tests__/Skills.third-party-gmail-sync.test.tsx index 5ff2d9856..a3042679c 100644 --- a/app/src/pages/__tests__/Skills.third-party-gmail-sync.test.tsx +++ b/app/src/pages/__tests__/Skills.third-party-gmail-sync.test.tsx @@ -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(, { initialEntries: ['/skills'] }); + fireEvent.click(screen.getByRole('tab', { name: 'Composio' })); const integrationsSection = screen .getByRole('heading', { name: 'Composio Integrations' }) diff --git a/app/src/pages/__tests__/Skills.third-party-notion-debug-tools.test.tsx b/app/src/pages/__tests__/Skills.third-party-notion-debug-tools.test.tsx index 994b72f74..e92be02ff 100644 --- a/app/src/pages/__tests__/Skills.third-party-notion-debug-tools.test.tsx +++ b/app/src/pages/__tests__/Skills.third-party-notion-debug-tools.test.tsx @@ -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(, { 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 }); diff --git a/app/src/services/api/__tests__/skillsApi.test.ts b/app/src/services/api/__tests__/skillsApi.test.ts index d614c0522..3dc1decf7 100644 --- a/app/src/services/api/__tests__/skillsApi.test.ts +++ b/app/src/services/api/__tests__/skillsApi.test.ts @@ -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'); diff --git a/app/src/services/api/skillsApi.ts b/app/src/services/api/skillsApi.ts index 4907ebaa9..e5a115ab9 100644 --- a/app/src/services/api/skillsApi.ts +++ b/app/src/services/api/skillsApi.ts @@ -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 & { + 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(response: Envelope | 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 => { @@ -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; }, /** diff --git a/app/test/playwright/specs/composio-triggers-flow.spec.ts b/app/test/playwright/specs/composio-triggers-flow.spec.ts index f9088d55d..b2dc9e3bd 100644 --- a/app/test/playwright/specs/composio-triggers-flow.spec.ts +++ b/app/test/playwright/specs/composio-triggers-flow.spec.ts @@ -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, }); diff --git a/app/test/playwright/specs/connector-gmail-composio.spec.ts b/app/test/playwright/specs/connector-gmail-composio.spec.ts index 564c7a382..060eeb42c 100644 --- a/app/test/playwright/specs/connector-gmail-composio.spec.ts +++ b/app/test/playwright/specs/connector-gmail-composio.spec.ts @@ -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; } diff --git a/app/test/playwright/specs/connector-session-guard-matrix.spec.ts b/app/test/playwright/specs/connector-session-guard-matrix.spec.ts index fad9966c9..43a9e60b5 100644 --- a/app/test/playwright/specs/connector-session-guard-matrix.spec.ts +++ b/app/test/playwright/specs/connector-session-guard-matrix.spec.ts @@ -72,6 +72,7 @@ async function bootSkills(page: Page, userId: string): Promise { 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/ ); diff --git a/app/test/playwright/specs/gmail-flow.spec.ts b/app/test/playwright/specs/gmail-flow.spec.ts index eeead75d5..9f379127e 100644 --- a/app/test/playwright/specs/gmail-flow.spec.ts +++ b/app/test/playwright/specs/gmail-flow.spec.ts @@ -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 }); diff --git a/app/test/playwright/specs/intelligence-memory-ui-functional.spec.ts b/app/test/playwright/specs/intelligence-memory-ui-functional.spec.ts index cd92e475c..cb56a72a5 100644 --- a/app/test/playwright/specs/intelligence-memory-ui-functional.spec.ts +++ b/app/test/playwright/specs/intelligence-memory-ui-functional.spec.ts @@ -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 }); diff --git a/app/test/playwright/specs/skills-registry.spec.ts b/app/test/playwright/specs/skills-registry.spec.ts index 687d75d8d..24903c199 100644 --- a/app/test/playwright/specs/skills-registry.spec.ts +++ b/app/test/playwright/specs/skills-registry.spec.ts @@ -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 }) => { diff --git a/src/openhuman/context/channels_prompt.rs b/src/openhuman/context/channels_prompt.rs index 9133a45bc..7d8905246 100644 --- a/src/openhuman/context/channels_prompt.rs +++ b/src/openhuman/context/channels_prompt.rs @@ -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, diff --git a/src/openhuman/monitor/ops.rs b/src/openhuman/monitor/ops.rs index ed7e5311d..001e25fda 100644 --- a/src/openhuman/monitor/ops.rs +++ b/src/openhuman/monitor/ops.rs @@ -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 ); } } diff --git a/src/openhuman/workflows/inject.rs b/src/openhuman/workflows/inject.rs index 512f995aa..bfcd059d7 100644 --- a/src/openhuman/workflows/inject.rs +++ b/src/openhuman/workflows/inject.rs @@ -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, diff --git a/src/openhuman/workflows/ops_discover.rs b/src/openhuman/workflows/ops_discover.rs index 208aa9a8c..f76336e2f 100644 --- a/src/openhuman/workflows/ops_discover.rs +++ b/src/openhuman/workflows/ops_discover.rs @@ -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 `/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 { + 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) { 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 { 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 { } 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 { @@ -257,8 +281,9 @@ fn load_skill_dir(dir: &Path, dir_name: &str, scope: WorkflowScope) -> Option, + skill_id: &str, +) -> Result { + let mut dir_match: Option = None; + let mut name_match: Option = 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")), + } +} diff --git a/src/openhuman/workflows/ops_parse.rs b/src/openhuman/workflows/ops_parse.rs index d91ff8458..6a336c5d1 100644 --- a/src/openhuman/workflows/ops_parse.rs +++ b/src/openhuman/workflows/ops_parse.rs @@ -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, diff --git a/src/openhuman/workflows/ops_tests.rs b/src/openhuman/workflows/ops_tests.rs index aa380d584..fac6d41a1 100644 --- a/src/openhuman/workflows/ops_tests.rs +++ b/src/openhuman/workflows/ops_tests.rs @@ -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"), ""); + 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"), + "", + ); + 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(); diff --git a/src/openhuman/workflows/ops_types.rs b/src/openhuman/workflows/ops_types.rs index 9768e3843..15eadd38f 100644 --- a/src/openhuman/workflows/ops_types.rs +++ b/src/openhuman/workflows/ops_types.rs @@ -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, #[serde(default)] pub compatibility: Option, + /// Platform compatibility hint used by Hermes-style skills. Absence means + /// all platforms. + #[serde(default)] + pub platforms: Vec, /// 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) -> Vec { + 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 { + 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, /// Tags declared in frontmatter. pub tags: Vec, + /// Compatible platform hints from SKILL.md frontmatter. + #[serde(default)] + pub platforms: Vec, + /// Related skill names declared by the ecosystem/catalog. + #[serde(default)] + pub related_skills: Vec, + /// 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, diff --git a/src/openhuman/workflows/schemas.rs b/src/openhuman/workflows/schemas.rs index 901aa920b..3796887be 100644 --- a/src/openhuman/workflows/schemas.rs +++ b/src/openhuman/workflows/schemas.rs @@ -111,6 +111,9 @@ struct WorkflowSummary { version: String, author: Option, tags: Vec, + platforms: Vec, + related_skills: Vec, + source_format: String, tools: Vec, prompts: Vec, location: Option, @@ -138,6 +141,17 @@ impl From 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()),