mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
feat(skills): add Hermes-style skill explorer (#3388)
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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': 'مفتاح القبول',
|
||||
|
||||
@@ -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': 'অ্যাক্সেপ্ট কী',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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': 'स्वीकार कुंजी',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': '수락 키',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': 'Клавиша принятия',
|
||||
|
||||
@@ -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,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 }) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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()),
|
||||
|
||||
Reference in New Issue
Block a user