mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-22 07:15:10 +00:00
feat(skills)!: promote Skills Repository out of experimental (#6579)
* feat(skills)!: promote Skills Repository out of experimental The registry-backed Skills Repository (crewai skill create/publish/ install/list, @org/name refs, global cache) is now mainline: - CLI: `crewai skill ...` is a top-level group; the CREWAI_EXPERIMENTAL gate and the now-empty `crewai experimental` group are removed. - Runtime: registry.py, cache.py, and events.py move from crewai.experimental.skills into crewai.skills next to the loader; the require_experimental_skills() gate is gone. crewai.experimental.skills remains as a deprecated re-export shim. - Docs: concepts/skills now leads with the CLI workflow and documents the create -> publish -> install lifecycle. Linear: n/a (requested promotion) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(skills): org-scoped publish only + docs in all languages Skills are always scoped to the publishing organization, like tools: drop the --public/--private flags from `crewai skill publish` and always send is_public=False to the registry. CLI tests assert the flag is rejected and the API never receives a public publish. Translate the new CLI-first Quick Start and the create -> publish -> install lifecycle section into ar, pt-BR, and ko concepts/skills docs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skills): address review comments on the promotion PR - Back-compat shim now aliases the old submodules in sys.modules so `crewai.experimental.skills.registry/cache/events` imports (and patch targets) resolve to the real crewai.skills modules, not just the package-root re-exports. - `crewai skill publish` actually enforces the git-state check that --force claims to skip: unsynced repos block publishing (mirroring tool publish); standalone skill dirs outside any git repo publish without a check. - Explicit UTF-8 encoding on SKILL.md and cache-metadata reads/writes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skills): fail closed when git state cannot be validated on publish Follow deploy's pattern: construct git.Repository(fetch=False) and only treat "not a Git repository" as skippable — any other git error (fetch/auth/misconfiguration) now blocks publish with a --force escape hatch instead of silently bypassing the sync check. Also single-style imports in the shim test (CodeQL) with the dotted shim import covered via importlib. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(skills): fetch before sync check on publish; bump mcp past advisories Publish now refreshes remote-tracking refs (repository.fetch()) before is_synced(), so ahead/behind is judged against the actual remote rather than stale local refs; a failing fetch blocks publish with the --force escape hatch. Adds a fail-closed test for fetch errors. Raise mcp to >=1.28.1,<2 (locks 1.28.1): the ~=1.26.0 pin blocked GHSA-hvrp-rf83-w775 / GHSA-jpw9-pfvf-9f58 (fixed 1.27.2) and GHSA-vj7q-gjh5-988w (fixed 1.28.1), which were failing pip-audit on this PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Vinicius Brasil <vini@hey.com>
This commit is contained in:
@@ -24,15 +24,23 @@ mode: "wide"
|
||||
|
||||
## البداية السريعة
|
||||
|
||||
### 1. إنشاء مجلد المهارة
|
||||
### 1. إنشاء مهارة باستخدام سطر الأوامر (CLI)
|
||||
|
||||
واجهة سطر الأوامر هي الطريقة المدعومة لإنشاء مهارة — فهي تُنشئ لك هيكل المجلد وملف `SKILL.md` صالحًا:
|
||||
|
||||
```shell Terminal
|
||||
crewai skill create code-review
|
||||
```
|
||||
|
||||
داخل مشروع طاقم (حيث يوجد `pyproject.toml`) يُنشئ هذا الأمر `./skills/code-review/`؛ وخارج المشروع يُنشئ `./code-review/` في المجلد الحالي (يمكنك فرض هذا السلوك باستخدام `--no-project`):
|
||||
|
||||
```
|
||||
skills/
|
||||
└── code-review/
|
||||
├── SKILL.md # مطلوب — التعليمات
|
||||
├── references/ # اختياري — مستندات مرجعية
|
||||
│ └── style-guide.md
|
||||
└── scripts/ # اختياري — سكربتات قابلة للتنفيذ
|
||||
├── SKILL.md # Required — instructions (pre-filled template)
|
||||
├── references/ # Optional — reference docs
|
||||
├── scripts/ # Optional — executable scripts
|
||||
└── assets/ # Optional — static files
|
||||
```
|
||||
|
||||
### 2. كتابة SKILL.md الخاص بك
|
||||
@@ -164,6 +172,65 @@ agent = Agent(
|
||||
|
||||
---
|
||||
|
||||
## إنشاء المهارات ونشرها وتثبيتها
|
||||
|
||||
للمهارات دورة حياة كاملة تُدار عبر واجهة سطر الأوامر: **أنشئها باستخدام `crewai skill create`، وانشرها باستخدام `crewai skill publish`** — إنشاء المجلدات يدويًا يصلح للتجارب المحلية، لكن واجهة سطر الأوامر هي سير العمل المقصود، وهي تحافظ على صحة هيكل المهارة وبياناتها الوصفية.
|
||||
|
||||
### الإنشاء
|
||||
|
||||
```shell Terminal
|
||||
crewai skill create my-skill
|
||||
```
|
||||
|
||||
يُنشئ هذا الأمر المجلد (داخل `./skills/` في مشروع الطاقم) مع قالب `SKILL.md`، بالإضافة إلى مجلدات فارغة `scripts/` و `references/` و `assets/`. عدّل `SKILL.md` لتعريف التعليمات.
|
||||
|
||||
### النشر
|
||||
|
||||
نفّذ الأمر من داخل مجلد المهارة (حيث يوجد `SKILL.md`):
|
||||
|
||||
```shell Terminal
|
||||
cd skills/my-skill
|
||||
crewai skill publish
|
||||
```
|
||||
|
||||
يقرأ النشر الحقول `name` و `description` و `metadata.version` من البيانات الوصفية في مقدمة `SKILL.md` ويدفع المهارة إلى سجل CrewAI. **المهارات المنشورة تكون دائمًا مقيّدة بنطاق مؤسستك** — مثل الأدوات، لا يستطيع رؤيتها وتثبيتها إلا أعضاء المؤسسة الناشرة؛ ولا توجد رؤية عامة. أعلام مفيدة:
|
||||
|
||||
| العلم | التأثير |
|
||||
| :--- | :--- |
|
||||
| `--org <slug>` | النشر تحت مؤسسة محددة (يتجاوز الإعدادات). |
|
||||
| `--force` | تخطي التحقق من حالة git (تغييرات غير مُثبتة، إلخ). |
|
||||
|
||||
### التثبيت
|
||||
|
||||
ثبّت مهارة منشورة عبر مرجعها `@org/name`:
|
||||
|
||||
```shell Terminal
|
||||
crewai skill install @acme/code-review
|
||||
```
|
||||
|
||||
داخل مشروع الطاقم تُثبَّت المهارة في `./skills/{name}/`؛ وخارج المشروع تذهب إلى ذاكرة التخزين المؤقتة المشتركة في `~/.crewai/skills/{org}/{name}/`.
|
||||
|
||||
يمكن للوكلاء أيضًا الإشارة إلى مهارات السجل مباشرة — يتم حلّها من ذاكرة التخزين المؤقتة المحلية (أو من مجلد `skills/` في المشروع) وقت التشغيل:
|
||||
|
||||
```python
|
||||
agent = Agent(
|
||||
role="Senior Code Reviewer",
|
||||
goal="Review pull requests for quality and security issues",
|
||||
backstory="Staff engineer with expertise in secure coding practices.",
|
||||
skills=["@acme/code-review"], # registry ref, resolved locally
|
||||
)
|
||||
```
|
||||
|
||||
### عرض القائمة
|
||||
|
||||
```shell Terminal
|
||||
crewai skill list
|
||||
```
|
||||
|
||||
يعرض المهارات المثبّتة من مجلد المشروع `./skills/` ومن ذاكرة التخزين المؤقتة العامة معًا، مع إصداراتها ومساراتها.
|
||||
|
||||
---
|
||||
|
||||
## المهارات على مستوى الطاقم
|
||||
|
||||
يمكن تعيين المهارات على الطاقم لتُطبّق على **جميع الوكلاء**:
|
||||
|
||||
@@ -24,15 +24,23 @@ You often need **both**: skills for expertise, tools for action. They are config
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Create a Skill Directory
|
||||
### 1. Create a Skill with the CLI
|
||||
|
||||
The CLI is the supported way to create a skill — it scaffolds the directory layout and a valid `SKILL.md` for you:
|
||||
|
||||
```shell Terminal
|
||||
crewai skill create code-review
|
||||
```
|
||||
|
||||
Inside a crew project (where `pyproject.toml` lives) this creates `./skills/code-review/`; outside a project it creates `./code-review/` in the current directory (you can force that behavior with `--no-project`):
|
||||
|
||||
```
|
||||
skills/
|
||||
└── code-review/
|
||||
├── SKILL.md # Required — instructions
|
||||
├── SKILL.md # Required — instructions (pre-filled template)
|
||||
├── references/ # Optional — reference docs
|
||||
│ └── style-guide.md
|
||||
└── scripts/ # Optional — executable scripts
|
||||
├── scripts/ # Optional — executable scripts
|
||||
└── assets/ # Optional — static files
|
||||
```
|
||||
|
||||
### 2. Write Your SKILL.md
|
||||
@@ -164,6 +172,65 @@ agent = Agent(
|
||||
|
||||
---
|
||||
|
||||
## Creating, Publishing, and Installing Skills
|
||||
|
||||
Skills have a full lifecycle managed by the CLI: **create them with `crewai skill create`, publish them with `crewai skill publish`** — hand-rolling directories works for local experiments, but the CLI is the intended workflow and keeps your skill layout and frontmatter valid.
|
||||
|
||||
### Create
|
||||
|
||||
```shell Terminal
|
||||
crewai skill create my-skill
|
||||
```
|
||||
|
||||
Scaffolds the directory (into `./skills/` inside a crew project) with a template `SKILL.md`, plus empty `scripts/`, `references/`, and `assets/` directories. Edit `SKILL.md` to define the instructions.
|
||||
|
||||
### Publish
|
||||
|
||||
Run from inside the skill directory (where `SKILL.md` is):
|
||||
|
||||
```shell Terminal
|
||||
cd skills/my-skill
|
||||
crewai skill publish
|
||||
```
|
||||
|
||||
Publishing reads `name`, `description`, and `metadata.version` from the `SKILL.md` frontmatter and pushes the skill to the CrewAI registry. **Published skills are always scoped to your organization** — like tools, only members of the publishing org can see and install them; there is no public visibility. Useful flags:
|
||||
|
||||
| Flag | Effect |
|
||||
| :--- | :--- |
|
||||
| `--org <slug>` | Publish under a specific organization (overrides settings). |
|
||||
| `--force` | Skip git-state validation (uncommitted changes, etc.). |
|
||||
|
||||
### Install
|
||||
|
||||
Install a published skill by its `@org/name` reference:
|
||||
|
||||
```shell Terminal
|
||||
crewai skill install @acme/code-review
|
||||
```
|
||||
|
||||
Inside a crew project the skill lands in `./skills/{name}/`; outside a project it goes to the shared cache at `~/.crewai/skills/{org}/{name}/`.
|
||||
|
||||
Agents can also reference registry skills directly — they resolve from the local cache (or project `skills/` directory) at runtime:
|
||||
|
||||
```python
|
||||
agent = Agent(
|
||||
role="Senior Code Reviewer",
|
||||
goal="Review pull requests for quality and security issues",
|
||||
backstory="Staff engineer with expertise in secure coding practices.",
|
||||
skills=["@acme/code-review"], # registry ref, resolved locally
|
||||
)
|
||||
```
|
||||
|
||||
### List
|
||||
|
||||
```shell Terminal
|
||||
crewai skill list
|
||||
```
|
||||
|
||||
Shows installed skills from both the project `./skills/` directory and the global cache, with their versions and paths.
|
||||
|
||||
---
|
||||
|
||||
## Crew-Level Skills
|
||||
|
||||
Skills can be set on a crew to apply to **all agents**:
|
||||
|
||||
@@ -24,15 +24,23 @@ mode: "wide"
|
||||
|
||||
## 빠른 시작
|
||||
|
||||
### 1. 스킬 디렉터리 생성
|
||||
### 1. CLI로 스킬 생성
|
||||
|
||||
CLI는 스킬을 생성하는 공식 지원 방식입니다 — 디렉터리 레이아웃과 유효한 `SKILL.md`를 자동으로 스캐폴딩해 줍니다:
|
||||
|
||||
```shell Terminal
|
||||
crewai skill create code-review
|
||||
```
|
||||
|
||||
크루 프로젝트 내부(`pyproject.toml`이 있는 곳)에서는 `./skills/code-review/`가 생성되고, 프로젝트 외부에서는 현재 디렉터리에 `./code-review/`가 생성됩니다 (`--no-project`로 이 동작을 강제할 수 있습니다):
|
||||
|
||||
```
|
||||
skills/
|
||||
└── code-review/
|
||||
├── SKILL.md # 필수 — 지침
|
||||
├── SKILL.md # 필수 — 지침 (미리 채워진 템플릿)
|
||||
├── references/ # 선택 — 참조 문서
|
||||
│ └── style-guide.md
|
||||
└── scripts/ # 선택 — 실행 가능한 스크립트
|
||||
├── scripts/ # 선택 — 실행 가능한 스크립트
|
||||
└── assets/ # 선택 — 정적 파일
|
||||
```
|
||||
|
||||
### 2. SKILL.md 작성
|
||||
@@ -164,6 +172,65 @@ agent = Agent(
|
||||
|
||||
---
|
||||
|
||||
## 스킬 생성, 게시 및 설치
|
||||
|
||||
스킬은 CLI로 관리되는 전체 라이프사이클을 갖습니다: **`crewai skill create`로 생성하고, `crewai skill publish`로 게시하세요** — 디렉터리를 직접 만드는 방식도 로컬 실험에는 사용할 수 있지만, CLI가 의도된 워크플로우이며 스킬 레이아웃과 프론트매터를 유효하게 유지해 줍니다.
|
||||
|
||||
### 생성
|
||||
|
||||
```shell Terminal
|
||||
crewai skill create my-skill
|
||||
```
|
||||
|
||||
디렉터리를 스캐폴딩하고 (크루 프로젝트 내부에서는 `./skills/`에 생성) 템플릿 `SKILL.md`와 함께 빈 `scripts/`, `references/`, `assets/` 디렉터리를 만듭니다. `SKILL.md`를 편집하여 지침을 정의하세요.
|
||||
|
||||
### 게시
|
||||
|
||||
스킬 디렉터리 내부(`SKILL.md`가 있는 곳)에서 실행하세요:
|
||||
|
||||
```shell Terminal
|
||||
cd skills/my-skill
|
||||
crewai skill publish
|
||||
```
|
||||
|
||||
게시 시 `SKILL.md` 프론트매터에서 `name`, `description`, `metadata.version`을 읽어 스킬을 CrewAI 레지스트리로 푸시합니다. **게시된 스킬은 항상 조직 범위로 제한됩니다** — 도구와 마찬가지로 게시한 조직의 멤버만 스킬을 보고 설치할 수 있으며, 공개 가시성은 없습니다. 유용한 플래그:
|
||||
|
||||
| 플래그 | 효과 |
|
||||
| :--- | :--- |
|
||||
| `--org <slug>` | 특정 조직으로 게시합니다 (설정을 재정의). |
|
||||
| `--force` | git 상태 검증을 건너뜁니다 (커밋되지 않은 변경 사항 등). |
|
||||
|
||||
### 설치
|
||||
|
||||
게시된 스킬을 `@org/name` 참조로 설치합니다:
|
||||
|
||||
```shell Terminal
|
||||
crewai skill install @acme/code-review
|
||||
```
|
||||
|
||||
크루 프로젝트 내부에서는 스킬이 `./skills/{name}/`에 설치되고, 프로젝트 외부에서는 공유 캐시인 `~/.crewai/skills/{org}/{name}/`에 저장됩니다.
|
||||
|
||||
에이전트는 레지스트리 스킬을 직접 참조할 수도 있습니다 — 런타임에 로컬 캐시(또는 프로젝트 `skills/` 디렉터리)에서 해석됩니다:
|
||||
|
||||
```python
|
||||
agent = Agent(
|
||||
role="Senior Code Reviewer",
|
||||
goal="Review pull requests for quality and security issues",
|
||||
backstory="Staff engineer with expertise in secure coding practices.",
|
||||
skills=["@acme/code-review"], # registry ref, resolved locally
|
||||
)
|
||||
```
|
||||
|
||||
### 목록 조회
|
||||
|
||||
```shell Terminal
|
||||
crewai skill list
|
||||
```
|
||||
|
||||
프로젝트 `./skills/` 디렉터리와 전역 캐시 양쪽에 설치된 스킬을 버전 및 경로와 함께 보여줍니다.
|
||||
|
||||
---
|
||||
|
||||
## 크루 레벨 스킬
|
||||
|
||||
스킬을 크루에 설정하여 **모든 에이전트**에 적용할 수 있습니다:
|
||||
|
||||
@@ -24,15 +24,23 @@ Frequentemente você precisa de **ambos**: skills para expertise, ferramentas pa
|
||||
|
||||
## Início Rápido
|
||||
|
||||
### 1. Crie um Diretório de Skill
|
||||
### 1. Crie uma Skill com a CLI
|
||||
|
||||
A CLI é a forma suportada de criar uma skill — ela gera a estrutura de diretórios e um `SKILL.md` válido para você:
|
||||
|
||||
```shell Terminal
|
||||
crewai skill create code-review
|
||||
```
|
||||
|
||||
Dentro de um projeto de crew (onde o `pyproject.toml` está) isso cria `./skills/code-review/`; fora de um projeto, cria `./code-review/` no diretório atual (você pode forçar esse comportamento com `--no-project`):
|
||||
|
||||
```
|
||||
skills/
|
||||
└── code-review/
|
||||
├── SKILL.md # Obrigatório — instruções
|
||||
├── SKILL.md # Obrigatório — instruções (template pré-preenchido)
|
||||
├── references/ # Opcional — documentos de referência
|
||||
│ └── style-guide.md
|
||||
└── scripts/ # Opcional — scripts executáveis
|
||||
├── scripts/ # Opcional — scripts executáveis
|
||||
└── assets/ # Opcional — arquivos estáticos
|
||||
```
|
||||
|
||||
### 2. Escreva seu SKILL.md
|
||||
@@ -164,6 +172,65 @@ agent = Agent(
|
||||
|
||||
---
|
||||
|
||||
## Criando, Publicando e Instalando Skills
|
||||
|
||||
Skills têm um ciclo de vida completo gerenciado pela CLI: **crie-as com `crewai skill create`, publique-as com `crewai skill publish`** — criar diretórios à mão funciona para experimentos locais, mas a CLI é o fluxo de trabalho pretendido e mantém a estrutura e o frontmatter da sua skill válidos.
|
||||
|
||||
### Criar
|
||||
|
||||
```shell Terminal
|
||||
crewai skill create my-skill
|
||||
```
|
||||
|
||||
Gera o diretório (em `./skills/` dentro de um projeto de crew) com um `SKILL.md` de template, além dos diretórios vazios `scripts/`, `references/` e `assets/`. Edite o `SKILL.md` para definir as instruções.
|
||||
|
||||
### Publicar
|
||||
|
||||
Execute de dentro do diretório da skill (onde o `SKILL.md` está):
|
||||
|
||||
```shell Terminal
|
||||
cd skills/my-skill
|
||||
crewai skill publish
|
||||
```
|
||||
|
||||
A publicação lê `name`, `description` e `metadata.version` do frontmatter do `SKILL.md` e envia a skill para o registro da CrewAI. **Skills publicadas são sempre escopadas à sua organização** — assim como ferramentas, apenas membros da organização que publicou podem vê-las e instalá-las; não há visibilidade pública. Flags úteis:
|
||||
|
||||
| Flag | Efeito |
|
||||
| :--- | :--- |
|
||||
| `--org <slug>` | Publica sob uma organização específica (sobrepõe as configurações). |
|
||||
| `--force` | Pula a validação de estado do git (alterações não commitadas, etc.). |
|
||||
|
||||
### Instalar
|
||||
|
||||
Instale uma skill publicada pela sua referência `@org/name`:
|
||||
|
||||
```shell Terminal
|
||||
crewai skill install @acme/code-review
|
||||
```
|
||||
|
||||
Dentro de um projeto de crew, a skill é colocada em `./skills/{name}/`; fora de um projeto, vai para o cache compartilhado em `~/.crewai/skills/{org}/{name}/`.
|
||||
|
||||
Agentes também podem referenciar skills do registro diretamente — elas são resolvidas a partir do cache local (ou do diretório `skills/` do projeto) em tempo de execução:
|
||||
|
||||
```python
|
||||
agent = Agent(
|
||||
role="Senior Code Reviewer",
|
||||
goal="Review pull requests for quality and security issues",
|
||||
backstory="Staff engineer with expertise in secure coding practices.",
|
||||
skills=["@acme/code-review"], # registry ref, resolved locally
|
||||
)
|
||||
```
|
||||
|
||||
### Listar
|
||||
|
||||
```shell Terminal
|
||||
crewai skill list
|
||||
```
|
||||
|
||||
Mostra as skills instaladas tanto do diretório `./skills/` do projeto quanto do cache global, com suas versões e caminhos.
|
||||
|
||||
---
|
||||
|
||||
## Skills no Nível do Crew
|
||||
|
||||
Skills podem ser definidas no crew para aplicar a **todos os agentes**:
|
||||
|
||||
@@ -686,20 +686,9 @@ def tool_publish(is_public: bool, force: bool) -> None:
|
||||
tool_cmd.publish(is_public, force)
|
||||
|
||||
|
||||
@crewai.group()
|
||||
def experimental() -> None:
|
||||
"""Experimental, unstable commands. Subject to change without notice."""
|
||||
import os
|
||||
|
||||
if os.environ.get("CREWAI_EXPERIMENTAL") != "1":
|
||||
raise click.UsageError(
|
||||
"Experimental commands are gated. Set CREWAI_EXPERIMENTAL=1 to enable."
|
||||
)
|
||||
|
||||
|
||||
@experimental.group(name="skill")
|
||||
@crewai.group(name="skill")
|
||||
def skill() -> None:
|
||||
"""Skill Repository related commands (experimental)."""
|
||||
"""Create, publish, and install agent skills."""
|
||||
|
||||
|
||||
@skill.command(name="create")
|
||||
@@ -713,7 +702,7 @@ def skill() -> None:
|
||||
help="Create skill in current dir instead of ./skills/",
|
||||
)
|
||||
def skill_create(name: str, in_project: bool) -> None:
|
||||
from crewai_cli.experimental.skills.main import SkillCommand
|
||||
from crewai_cli.skills.main import SkillCommand
|
||||
|
||||
skill_cmd = SkillCommand()
|
||||
skill_cmd.create(name, in_project=in_project)
|
||||
@@ -722,7 +711,7 @@ def skill_create(name: str, in_project: bool) -> None:
|
||||
@skill.command(name="install")
|
||||
@click.argument("ref")
|
||||
def skill_install(ref: str) -> None:
|
||||
from crewai_cli.experimental.skills.main import SkillCommand
|
||||
from crewai_cli.skills.main import SkillCommand
|
||||
|
||||
skill_cmd = SkillCommand()
|
||||
skill_cmd.install(ref)
|
||||
@@ -736,20 +725,19 @@ def skill_install(ref: str) -> None:
|
||||
show_default=True,
|
||||
help="Skip git-state validation.",
|
||||
)
|
||||
@click.option("--public", "is_public", flag_value=True, default=False)
|
||||
@click.option("--private", "is_public", flag_value=False)
|
||||
@click.option("--org", default=None, help="Organisation slug (overrides settings).")
|
||||
def skill_publish(is_public: bool, org: str | None, force: bool) -> None:
|
||||
from crewai_cli.experimental.skills.main import SkillCommand
|
||||
def skill_publish(org: str | None, force: bool) -> None:
|
||||
"""Publish the skill in the current directory, scoped to your organization."""
|
||||
from crewai_cli.skills.main import SkillCommand
|
||||
|
||||
skill_cmd = SkillCommand()
|
||||
skill_cmd.publish(is_public, org=org, force=force)
|
||||
skill_cmd.publish(org=org, force=force)
|
||||
|
||||
|
||||
@skill.command(name="list")
|
||||
def skill_list() -> None:
|
||||
"""List locally installed skills."""
|
||||
from crewai_cli.experimental.skills.main import SkillCommand
|
||||
from crewai_cli.skills.main import SkillCommand
|
||||
|
||||
skill_cmd = SkillCommand()
|
||||
skill_cmd.list_cached()
|
||||
|
||||
@@ -13,6 +13,7 @@ import zipfile
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
from crewai_cli import git
|
||||
from crewai_cli.command import BaseCommand, PlusAPIMixin
|
||||
from crewai_cli.config import Settings
|
||||
from crewai_cli.constants import DEFAULT_CREWAI_ENTERPRISE_URL
|
||||
@@ -63,7 +64,7 @@ class SkillCommand(BaseCommand, PlusAPIMixin):
|
||||
(skill_dir / "assets").mkdir()
|
||||
|
||||
skill_md = skill_dir / "SKILL.md"
|
||||
skill_md.write_text(_SKILL_MD_TEMPLATE.format(name=name))
|
||||
skill_md.write_text(_SKILL_MD_TEMPLATE.format(name=name), encoding="utf-8")
|
||||
|
||||
console.print(
|
||||
f"[green]Created skill [bold]{name}[/bold] at [bold]{skill_dir}[/bold].[/green]"
|
||||
@@ -148,7 +149,7 @@ class SkillCommand(BaseCommand, PlusAPIMixin):
|
||||
)
|
||||
else:
|
||||
try:
|
||||
from crewai.experimental.skills.cache import SkillCacheManager
|
||||
from crewai.skills.cache import SkillCacheManager
|
||||
|
||||
cache = SkillCacheManager()
|
||||
cache.store(org, name, version, archive_bytes)
|
||||
@@ -170,13 +171,19 @@ class SkillCommand(BaseCommand, PlusAPIMixin):
|
||||
"version": version,
|
||||
"installed_at": datetime.now(tz=timezone.utc).isoformat(),
|
||||
}
|
||||
(cache_dir / ".crewai_meta.json").write_text(json.dumps(meta, indent=2))
|
||||
(cache_dir / ".crewai_meta.json").write_text(
|
||||
json.dumps(meta, indent=2), encoding="utf-8"
|
||||
)
|
||||
console.print(
|
||||
f"[green]Installed [bold]{ref}[/bold]{' (' + version + ')' if version else ''} to global cache.[/green]"
|
||||
)
|
||||
|
||||
def publish(self, is_public: bool, org: str | None, force: bool = False) -> None:
|
||||
"""Publish the skill in the current directory to the registry."""
|
||||
def publish(self, org: str | None = None, force: bool = False) -> None:
|
||||
"""Publish the skill in the current directory to the registry.
|
||||
|
||||
Skills are always scoped to the publishing organization; there is no
|
||||
public visibility option.
|
||||
"""
|
||||
skill_md = Path("SKILL.md")
|
||||
if not skill_md.exists():
|
||||
console.print(
|
||||
@@ -185,8 +192,44 @@ class SkillCommand(BaseCommand, PlusAPIMixin):
|
||||
)
|
||||
raise SystemExit(1)
|
||||
|
||||
if not force:
|
||||
try:
|
||||
repository = git.Repository(fetch=False)
|
||||
except ValueError as exc:
|
||||
if "not a Git repository" not in str(exc):
|
||||
console.print(
|
||||
f"[red]Unable to validate git state: {exc}\n"
|
||||
"Fix the issue or pass --force to skip this check.[/red]"
|
||||
)
|
||||
raise SystemExit(1) from exc
|
||||
# Standalone skill directories may live outside any git repo;
|
||||
# there is no git state to validate in that case.
|
||||
repository = None
|
||||
if repository is not None:
|
||||
try:
|
||||
# Refresh remote-tracking refs so is_synced() compares
|
||||
# against the actual remote, not stale local state.
|
||||
repository.fetch()
|
||||
except ValueError as exc:
|
||||
console.print(
|
||||
f"[red]Unable to validate git state: {exc}\n"
|
||||
"Fix the issue or pass --force to skip this check.[/red]"
|
||||
)
|
||||
raise SystemExit(1) from exc
|
||||
if repository is not None and not repository.is_synced():
|
||||
console.print(
|
||||
"[bold red]Failed to publish skill.[/bold red]\n"
|
||||
"Local changes need to be resolved before publishing. Please do the following:\n"
|
||||
"* [bold]Commit[/bold] your changes.\n"
|
||||
"* [bold]Push[/bold] to sync with the remote.\n"
|
||||
"* [bold]Pull[/bold] the latest changes from the remote.\n"
|
||||
"\nOnce your repository is up-to-date, retry publishing the skill "
|
||||
"(or pass --force to skip this check)."
|
||||
)
|
||||
raise SystemExit(1)
|
||||
|
||||
try:
|
||||
frontmatter = self._parse_frontmatter(skill_md.read_text())
|
||||
frontmatter = self._parse_frontmatter(skill_md.read_text(encoding="utf-8"))
|
||||
except ValueError as exc:
|
||||
console.print(f"[red]Failed to parse SKILL.md frontmatter: {exc}[/red]")
|
||||
raise SystemExit(1) from exc
|
||||
@@ -233,7 +276,7 @@ class SkillCommand(BaseCommand, PlusAPIMixin):
|
||||
org=effective_org,
|
||||
name=name,
|
||||
version=version,
|
||||
is_public=is_public,
|
||||
is_public=False,
|
||||
description=description,
|
||||
encoded_file=encoded_file,
|
||||
)
|
||||
@@ -277,7 +320,7 @@ class SkillCommand(BaseCommand, PlusAPIMixin):
|
||||
meta_file = skill_dir / ".crewai_meta.json"
|
||||
if meta_file.exists():
|
||||
try:
|
||||
meta = json.loads(meta_file.read_text())
|
||||
meta = json.loads(meta_file.read_text(encoding="utf-8"))
|
||||
table.add_row(
|
||||
"cache",
|
||||
f"@{meta['org']}/{meta['name']}",
|
||||
@@ -368,7 +411,7 @@ class SkillCommand(BaseCommand, PlusAPIMixin):
|
||||
def _read_version(self, skill_md: Path) -> str | None:
|
||||
"""Read the version from a SKILL.md file's metadata, or None."""
|
||||
try:
|
||||
fm = self._parse_frontmatter(skill_md.read_text())
|
||||
fm = self._parse_frontmatter(skill_md.read_text(encoding="utf-8"))
|
||||
raw_metadata = fm.get("metadata")
|
||||
if isinstance(raw_metadata, dict):
|
||||
return raw_metadata.get("version")
|
||||
63
lib/cli/tests/skills/test_cli_commands.py
Normal file
63
lib/cli/tests/skills/test_cli_commands.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""Tests for the top-level `crewai skill` command group.
|
||||
|
||||
The Skills Repository graduated from the gated `crewai experimental skill`
|
||||
group: the commands must be reachable at `crewai skill ...` without
|
||||
CREWAI_EXPERIMENTAL set.
|
||||
"""
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from click.testing import CliRunner
|
||||
from crewai_cli.cli import crewai
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def runner():
|
||||
return CliRunner()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def no_experimental_env(monkeypatch):
|
||||
monkeypatch.delenv("CREWAI_EXPERIMENTAL", raising=False)
|
||||
|
||||
|
||||
class TestSkillGroupIsTopLevel:
|
||||
def test_skill_group_registered_at_top_level(self, runner, no_experimental_env):
|
||||
result = runner.invoke(crewai, ["skill", "--help"])
|
||||
assert result.exit_code == 0, result.output
|
||||
for subcommand in ("create", "install", "publish", "list"):
|
||||
assert subcommand in result.output
|
||||
|
||||
def test_skill_group_not_gated_by_experimental_env(
|
||||
self, runner, no_experimental_env
|
||||
):
|
||||
with mock.patch("crewai_cli.skills.main.SkillCommand") as mock_cmd:
|
||||
result = runner.invoke(crewai, ["skill", "create", "my-skill"])
|
||||
assert result.exit_code == 0, result.output
|
||||
mock_cmd.return_value.create.assert_called_once_with(
|
||||
"my-skill", in_project=True
|
||||
)
|
||||
|
||||
def test_skill_create_no_project_flag(self, runner, no_experimental_env):
|
||||
with mock.patch("crewai_cli.skills.main.SkillCommand") as mock_cmd:
|
||||
result = runner.invoke(crewai, ["skill", "create", "my-skill", "--no-project"])
|
||||
assert result.exit_code == 0, result.output
|
||||
mock_cmd.return_value.create.assert_called_once_with(
|
||||
"my-skill", in_project=False
|
||||
)
|
||||
|
||||
|
||||
class TestSkillPublishIsOrgScopedOnly:
|
||||
def test_publish_rejects_public_flag(self, runner, no_experimental_env):
|
||||
result = runner.invoke(crewai, ["skill", "publish", "--public"])
|
||||
assert result.exit_code != 0
|
||||
assert "No such option" in result.output
|
||||
|
||||
def test_publish_passes_org_and_force_only(self, runner, no_experimental_env):
|
||||
with mock.patch("crewai_cli.skills.main.SkillCommand") as mock_cmd:
|
||||
result = runner.invoke(
|
||||
crewai, ["skill", "publish", "--org", "acme", "--force"]
|
||||
)
|
||||
assert result.exit_code == 0, result.output
|
||||
mock_cmd.return_value.publish.assert_called_once_with(org="acme", force=True)
|
||||
@@ -36,7 +36,7 @@ def skill_command():
|
||||
TokenManager().save_tokens(
|
||||
"test-token", (datetime.now() + timedelta(seconds=36000)).timestamp()
|
||||
)
|
||||
from crewai_cli.experimental.skills.main import SkillCommand
|
||||
from crewai_cli.skills.main import SkillCommand
|
||||
cmd = SkillCommand()
|
||||
yield cmd
|
||||
|
||||
@@ -113,7 +113,7 @@ class TestSkillPublish:
|
||||
def test_publish_no_skill_md(self, skill_command):
|
||||
with in_temp_dir():
|
||||
with pytest.raises(SystemExit):
|
||||
skill_command.publish(is_public=True, org="acme")
|
||||
skill_command.publish(org="acme")
|
||||
|
||||
def test_publish_missing_version(self, skill_command):
|
||||
with in_temp_dir():
|
||||
@@ -121,7 +121,7 @@ class TestSkillPublish:
|
||||
"---\nname: my-skill\ndescription: Test.\n---\nInstructions."
|
||||
)
|
||||
with pytest.raises(SystemExit):
|
||||
skill_command.publish(is_public=True, org="acme")
|
||||
skill_command.publish(org="acme")
|
||||
|
||||
def test_publish_missing_name(self, skill_command):
|
||||
with in_temp_dir():
|
||||
@@ -129,7 +129,7 @@ class TestSkillPublish:
|
||||
"---\ndescription: Test.\nversion: 1.0.0\n---\nInstructions."
|
||||
)
|
||||
with pytest.raises(SystemExit):
|
||||
skill_command.publish(is_public=True, org="acme")
|
||||
skill_command.publish(org="acme")
|
||||
|
||||
def test_publish_no_org(self, skill_command):
|
||||
with in_temp_dir():
|
||||
@@ -142,11 +142,11 @@ class TestSkillPublish:
|
||||
mock_resp.status_code = 200
|
||||
mock_resp.json.return_value = {}
|
||||
mock_client.publish_skill.return_value = mock_resp
|
||||
with patch("crewai_cli.experimental.skills.main.Settings") as mock_settings_cls:
|
||||
with patch("crewai_cli.skills.main.Settings") as mock_settings_cls:
|
||||
mock_settings_cls.return_value.org_name = None
|
||||
mock_settings_cls.return_value.enterprise_base_url = None
|
||||
with pytest.raises(SystemExit):
|
||||
skill_command.publish(is_public=True, org=None)
|
||||
skill_command.publish(org=None)
|
||||
|
||||
def test_publish_calls_api(self, skill_command):
|
||||
with in_temp_dir():
|
||||
@@ -158,16 +158,102 @@ class TestSkillPublish:
|
||||
mock_resp.status_code = 200
|
||||
mock_resp.json.return_value = {}
|
||||
skill_command.plus_api_client.publish_skill = MagicMock(return_value=mock_resp)
|
||||
with patch("crewai_cli.experimental.skills.main.Settings") as mock_settings_cls:
|
||||
with patch("crewai_cli.skills.main.Settings") as mock_settings_cls:
|
||||
mock_settings_cls.return_value.org_name = "acme"
|
||||
mock_settings_cls.return_value.enterprise_base_url = None
|
||||
|
||||
skill_command.publish(is_public=False, org="acme")
|
||||
skill_command.publish(org="acme")
|
||||
|
||||
skill_command.plus_api_client.publish_skill.assert_called_once()
|
||||
call_kwargs = skill_command.plus_api_client.publish_skill.call_args
|
||||
assert call_kwargs.kwargs["name"] == "my-skill"
|
||||
assert call_kwargs.kwargs["version"] == "1.0.0"
|
||||
# Skills are always org-scoped; there is no public visibility.
|
||||
assert call_kwargs.kwargs["is_public"] is False
|
||||
|
||||
def test_publish_blocked_when_git_not_synced(self, skill_command):
|
||||
with in_temp_dir():
|
||||
Path("SKILL.md").write_text(
|
||||
"---\nname: my-skill\ndescription: A test skill.\nmetadata:\n version: 1.0.0\n---\nInstructions."
|
||||
)
|
||||
skill_command.plus_api_client.publish_skill = MagicMock()
|
||||
with patch("crewai_cli.skills.main.git.Repository") as mock_repo_cls:
|
||||
mock_repo_cls.return_value.is_synced.return_value = False
|
||||
with pytest.raises(SystemExit):
|
||||
skill_command.publish(org="acme")
|
||||
skill_command.plus_api_client.publish_skill.assert_not_called()
|
||||
|
||||
def test_publish_force_skips_git_check(self, skill_command):
|
||||
with in_temp_dir():
|
||||
Path("SKILL.md").write_text(
|
||||
"---\nname: my-skill\ndescription: A test skill.\nmetadata:\n version: 1.0.0\n---\nInstructions."
|
||||
)
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.is_success = True
|
||||
mock_resp.status_code = 200
|
||||
mock_resp.json.return_value = {}
|
||||
skill_command.plus_api_client.publish_skill = MagicMock(return_value=mock_resp)
|
||||
with (
|
||||
patch("crewai_cli.skills.main.git.Repository") as mock_repo_cls,
|
||||
patch("crewai_cli.skills.main.Settings") as mock_settings_cls,
|
||||
):
|
||||
mock_settings_cls.return_value.org_name = "acme"
|
||||
mock_settings_cls.return_value.enterprise_base_url = None
|
||||
skill_command.publish(org="acme", force=True)
|
||||
mock_repo_cls.assert_not_called()
|
||||
skill_command.plus_api_client.publish_skill.assert_called_once()
|
||||
|
||||
def test_publish_blocked_when_fetch_fails(self, skill_command):
|
||||
"""A failing remote fetch must fail closed, not silently skip syncing."""
|
||||
with in_temp_dir():
|
||||
Path("SKILL.md").write_text(
|
||||
"---\nname: my-skill\ndescription: A test skill.\nmetadata:\n version: 1.0.0\n---\nInstructions."
|
||||
)
|
||||
skill_command.plus_api_client.publish_skill = MagicMock()
|
||||
with patch("crewai_cli.skills.main.git.Repository") as mock_repo_cls:
|
||||
mock_repo_cls.return_value.fetch.side_effect = ValueError(
|
||||
"Git fetch failed with exit code 128"
|
||||
)
|
||||
with pytest.raises(SystemExit):
|
||||
skill_command.publish(org="acme")
|
||||
skill_command.plus_api_client.publish_skill.assert_not_called()
|
||||
|
||||
def test_publish_blocked_when_git_state_cannot_be_validated(self, skill_command):
|
||||
"""Git errors other than 'not a repo' must fail closed, not skip the check."""
|
||||
with in_temp_dir():
|
||||
Path("SKILL.md").write_text(
|
||||
"---\nname: my-skill\ndescription: A test skill.\nmetadata:\n version: 1.0.0\n---\nInstructions."
|
||||
)
|
||||
skill_command.plus_api_client.publish_skill = MagicMock()
|
||||
with patch(
|
||||
"crewai_cli.skills.main.git.Repository",
|
||||
side_effect=ValueError("Git fetch failed with exit code 128"),
|
||||
):
|
||||
with pytest.raises(SystemExit):
|
||||
skill_command.publish(org="acme")
|
||||
skill_command.plus_api_client.publish_skill.assert_not_called()
|
||||
|
||||
def test_publish_proceeds_outside_git_repo(self, skill_command):
|
||||
with in_temp_dir():
|
||||
Path("SKILL.md").write_text(
|
||||
"---\nname: my-skill\ndescription: A test skill.\nmetadata:\n version: 1.0.0\n---\nInstructions."
|
||||
)
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.is_success = True
|
||||
mock_resp.status_code = 200
|
||||
mock_resp.json.return_value = {}
|
||||
skill_command.plus_api_client.publish_skill = MagicMock(return_value=mock_resp)
|
||||
with (
|
||||
patch(
|
||||
"crewai_cli.skills.main.git.Repository",
|
||||
side_effect=ValueError("not a Git repository"),
|
||||
),
|
||||
patch("crewai_cli.skills.main.Settings") as mock_settings_cls,
|
||||
):
|
||||
mock_settings_cls.return_value.org_name = "acme"
|
||||
mock_settings_cls.return_value.enterprise_base_url = None
|
||||
skill_command.publish(org="acme")
|
||||
skill_command.plus_api_client.publish_skill.assert_called_once()
|
||||
|
||||
|
||||
class TestSkillListCached:
|
||||
@@ -14,7 +14,7 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai_cli.experimental.skills.main import _safe_extractall
|
||||
from crewai_cli.skills.main import _safe_extractall
|
||||
|
||||
|
||||
def _tar_from_members(build) -> tarfile.TarFile:
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
"""Experimental Skills Repository — registry refs, global cache, downloads.
|
||||
"""Deprecated location for the Skills Repository.
|
||||
|
||||
This package contains the registry-backed pieces of the skills feature
|
||||
(`@org/name` refs, `~/.crewai/skills/` cache, download events). The stable
|
||||
filesystem-based skill loader still lives in `crewai.skills`.
|
||||
The registry-backed skills feature graduated out of experimental; it now
|
||||
lives in `crewai.skills` alongside the filesystem-based loader. This module
|
||||
re-exports the public names and aliases the old submodules so imports
|
||||
written against the experimental namespace keep working, and will be
|
||||
removed in a future release.
|
||||
"""
|
||||
|
||||
from crewai.experimental.skills.cache import SkillCacheManager
|
||||
from crewai.experimental.skills.registry import (
|
||||
import sys
|
||||
|
||||
from crewai.skills import cache, events, registry
|
||||
from crewai.skills.cache import SkillCacheManager
|
||||
from crewai.skills.registry import (
|
||||
SkillNotCachedError,
|
||||
is_registry_ref,
|
||||
parse_registry_ref,
|
||||
@@ -14,10 +19,19 @@ from crewai.experimental.skills.registry import (
|
||||
)
|
||||
|
||||
|
||||
# Keep `crewai.experimental.skills.<module>` imports (and patch targets)
|
||||
# resolving to the real modules in crewai.skills.
|
||||
sys.modules[__name__ + ".cache"] = cache
|
||||
sys.modules[__name__ + ".events"] = events
|
||||
sys.modules[__name__ + ".registry"] = registry
|
||||
|
||||
__all__ = [
|
||||
"SkillCacheManager",
|
||||
"SkillNotCachedError",
|
||||
"cache",
|
||||
"events",
|
||||
"is_registry_ref",
|
||||
"parse_registry_ref",
|
||||
"registry",
|
||||
"resolve_registry_ref",
|
||||
]
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
"""Experimental feature gate for the Skills Repository."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
|
||||
ENV_VAR = "CREWAI_EXPERIMENTAL"
|
||||
|
||||
|
||||
class ExperimentalFeatureDisabledError(RuntimeError):
|
||||
"""Raised when an experimental feature is used without the flag set."""
|
||||
|
||||
|
||||
def is_enabled() -> bool:
|
||||
return os.environ.get(ENV_VAR) == "1"
|
||||
|
||||
|
||||
def require_experimental_skills() -> None:
|
||||
if not is_enabled():
|
||||
raise ExperimentalFeatureDisabledError(
|
||||
"The Skills Repository (registry refs, cache, downloads) is "
|
||||
f"experimental. Set {ENV_VAR}=1 to enable it."
|
||||
)
|
||||
@@ -1,8 +1,11 @@
|
||||
"""Agent Skills standard implementation for crewAI.
|
||||
|
||||
Provides filesystem-based skill packaging with progressive disclosure.
|
||||
Provides filesystem-based skill packaging with progressive disclosure, plus
|
||||
the registry-backed Skills Repository (`@org/name` refs, global cache,
|
||||
downloads).
|
||||
"""
|
||||
|
||||
from crewai.skills.cache import SkillCacheManager
|
||||
from crewai.skills.loader import (
|
||||
activate_skill,
|
||||
discover_skills,
|
||||
@@ -11,14 +14,25 @@ from crewai.skills.loader import (
|
||||
)
|
||||
from crewai.skills.models import Skill, SkillFrontmatter
|
||||
from crewai.skills.parser import SkillParseError
|
||||
from crewai.skills.registry import (
|
||||
SkillNotCachedError,
|
||||
is_registry_ref,
|
||||
parse_registry_ref,
|
||||
resolve_registry_ref,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Skill",
|
||||
"SkillCacheManager",
|
||||
"SkillFrontmatter",
|
||||
"SkillNotCachedError",
|
||||
"SkillParseError",
|
||||
"activate_skill",
|
||||
"discover_skills",
|
||||
"is_registry_ref",
|
||||
"load_skill",
|
||||
"load_skills",
|
||||
"parse_registry_ref",
|
||||
"resolve_registry_ref",
|
||||
]
|
||||
|
||||
@@ -88,7 +88,9 @@ class SkillCacheManager:
|
||||
"version": version,
|
||||
"installed_at": datetime.now(tz=timezone.utc).isoformat(),
|
||||
}
|
||||
(skill_dir / _META_FILENAME).write_text(json.dumps(meta, indent=2))
|
||||
(skill_dir / _META_FILENAME).write_text(
|
||||
json.dumps(meta, indent=2), encoding="utf-8"
|
||||
)
|
||||
return skill_dir
|
||||
|
||||
def list_cached(self) -> list[SkillMetadata]:
|
||||
@@ -103,7 +105,9 @@ class SkillCacheManager:
|
||||
meta_file = skill_dir / _META_FILENAME
|
||||
if meta_file.exists():
|
||||
try:
|
||||
results.append(json.loads(meta_file.read_text()))
|
||||
results.append(
|
||||
json.loads(meta_file.read_text(encoding="utf-8"))
|
||||
)
|
||||
except (json.JSONDecodeError, KeyError):
|
||||
_logger.debug(
|
||||
"Skipping malformed cache entry: %s",
|
||||
@@ -164,7 +164,7 @@ def load_skill(
|
||||
for s in discover_skills(skill, source=source)
|
||||
]
|
||||
if isinstance(skill, str) and skill.startswith("@"):
|
||||
from crewai.experimental.skills.registry import resolve_registry_ref
|
||||
from crewai.skills.registry import resolve_registry_ref
|
||||
|
||||
return [resolve_registry_ref(skill, source=source)]
|
||||
if isinstance(skill, str) and skill.lstrip().startswith("---\n"):
|
||||
@@ -202,7 +202,7 @@ def load_skills(
|
||||
for skill in load_skill(skill_input, source=source):
|
||||
dedup_key = skill.name
|
||||
if isinstance(skill_input, str) and skill_input.startswith("@"):
|
||||
from crewai.experimental.skills.registry import parse_registry_ref
|
||||
from crewai.skills.registry import parse_registry_ref
|
||||
|
||||
org, _ = parse_registry_ref(skill_input)
|
||||
dedup_key = f"{org}/{skill.name}"
|
||||
|
||||
@@ -11,7 +11,7 @@ from pathlib import Path
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from crewai.experimental.skills.cache import SkillCacheManager
|
||||
from crewai.skills.cache import SkillCacheManager
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
@@ -100,11 +100,9 @@ def resolve_registry_ref(
|
||||
Raises:
|
||||
SkillNotCachedError: When not cached and running in non-interactive mode.
|
||||
"""
|
||||
from crewai.experimental.skills._flag import require_experimental_skills
|
||||
from crewai.skills.loader import activate_skill
|
||||
from crewai.skills.parser import load_skill_metadata
|
||||
|
||||
require_experimental_skills()
|
||||
org, name = parse_registry_ref(ref)
|
||||
|
||||
local_path = Path.cwd() / "skills" / name
|
||||
@@ -154,7 +152,7 @@ def download_skill(
|
||||
|
||||
try:
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
from crewai.experimental.skills.events import (
|
||||
from crewai.skills.events import (
|
||||
SkillDownloadCompletedEvent,
|
||||
SkillDownloadStartedEvent,
|
||||
)
|
||||
@@ -1,6 +0,0 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _enable_experimental_skills(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("CREWAI_EXPERIMENTAL", "1")
|
||||
@@ -1,30 +0,0 @@
|
||||
"""Tests for the CREWAI_EXPERIMENTAL gate on Skills Repository."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai.experimental.skills._flag import (
|
||||
ExperimentalFeatureDisabledError,
|
||||
require_experimental_skills,
|
||||
)
|
||||
from crewai.experimental.skills.registry import resolve_registry_ref
|
||||
|
||||
|
||||
def test_require_raises_without_flag(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.delenv("CREWAI_EXPERIMENTAL", raising=False)
|
||||
with pytest.raises(ExperimentalFeatureDisabledError):
|
||||
require_experimental_skills()
|
||||
|
||||
|
||||
def test_resolve_registry_ref_raises_without_flag(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.delenv("CREWAI_EXPERIMENTAL", raising=False)
|
||||
with pytest.raises(ExperimentalFeatureDisabledError):
|
||||
resolve_registry_ref("@acme/my-skill")
|
||||
|
||||
|
||||
def test_require_passes_with_flag(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setenv("CREWAI_EXPERIMENTAL", "1")
|
||||
require_experimental_skills()
|
||||
@@ -10,7 +10,7 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai.experimental.skills.cache import SkillCacheManager, _safe_extractall
|
||||
from crewai.skills.cache import SkillCacheManager, _safe_extractall
|
||||
|
||||
|
||||
def _make_tar_gz(files: dict[str, str]) -> bytes:
|
||||
43
lib/crewai/tests/skills/test_experimental_shim.py
Normal file
43
lib/crewai/tests/skills/test_experimental_shim.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""The old experimental namespace must keep re-exporting the public names."""
|
||||
|
||||
|
||||
def test_experimental_namespace_reexports_public_names():
|
||||
from crewai.experimental import skills as experimental_skills
|
||||
from crewai.skills.cache import SkillCacheManager
|
||||
from crewai.skills.registry import (
|
||||
SkillNotCachedError,
|
||||
is_registry_ref,
|
||||
parse_registry_ref,
|
||||
resolve_registry_ref,
|
||||
)
|
||||
|
||||
assert experimental_skills.SkillCacheManager is SkillCacheManager
|
||||
assert experimental_skills.SkillNotCachedError is SkillNotCachedError
|
||||
assert experimental_skills.is_registry_ref is is_registry_ref
|
||||
assert experimental_skills.parse_registry_ref is parse_registry_ref
|
||||
assert experimental_skills.resolve_registry_ref is resolve_registry_ref
|
||||
|
||||
|
||||
def test_experimental_submodule_imports_alias_real_modules():
|
||||
"""Old submodule import style must resolve to the crewai.skills modules."""
|
||||
import importlib
|
||||
|
||||
from crewai.experimental.skills import cache as shim_cache
|
||||
from crewai.experimental.skills import events as shim_events
|
||||
from crewai.experimental.skills import registry as shim_registry
|
||||
from crewai.experimental.skills.cache import SkillCacheManager
|
||||
from crewai.experimental.skills.registry import resolve_registry_ref
|
||||
from crewai.skills import cache as real_cache
|
||||
from crewai.skills import events as real_events
|
||||
from crewai.skills import registry as real_registry
|
||||
|
||||
assert shim_cache is real_cache
|
||||
assert shim_events is real_events
|
||||
assert shim_registry is real_registry
|
||||
assert SkillCacheManager is real_cache.SkillCacheManager
|
||||
assert resolve_registry_ref is real_registry.resolve_registry_ref
|
||||
# Dotted-path import resolves through sys.modules to the same module.
|
||||
assert (
|
||||
importlib.import_module("crewai.experimental.skills.registry")
|
||||
is real_registry
|
||||
)
|
||||
@@ -197,7 +197,7 @@ class TestLoadSkill:
|
||||
}[ref]
|
||||
|
||||
monkeypatch.setattr(
|
||||
"crewai.experimental.skills.registry.resolve_registry_ref",
|
||||
"crewai.skills.registry.resolve_registry_ref",
|
||||
resolve_registry_ref,
|
||||
)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from crewai.experimental.skills.registry import (
|
||||
from crewai.skills.registry import (
|
||||
SkillNotCachedError,
|
||||
is_registry_ref,
|
||||
parse_registry_ref,
|
||||
@@ -75,11 +75,11 @@ class TestResolveRegistryRef:
|
||||
mock_cache.get_cached_path.return_value = None
|
||||
|
||||
with (
|
||||
patch("crewai.experimental.skills.registry._is_noninteractive", return_value=False),
|
||||
patch("crewai.skills.registry._is_noninteractive", return_value=False),
|
||||
patch.object(Path, "cwd", return_value=tmp_path),
|
||||
patch("crewai.experimental.skills.registry.SkillCacheManager", return_value=mock_cache),
|
||||
patch("crewai.skills.registry.SkillCacheManager", return_value=mock_cache),
|
||||
):
|
||||
from crewai.experimental.skills.registry import resolve_registry_ref
|
||||
from crewai.skills.registry import resolve_registry_ref
|
||||
skill = resolve_registry_ref("@acme/my-skill")
|
||||
|
||||
assert skill.name == "my-skill"
|
||||
@@ -90,11 +90,11 @@ class TestResolveRegistryRef:
|
||||
mock_cache.get_cached_path.return_value = None
|
||||
|
||||
with (
|
||||
patch("crewai.experimental.skills.registry._is_noninteractive", return_value=True),
|
||||
patch("crewai.skills.registry._is_noninteractive", return_value=True),
|
||||
patch.object(Path, "cwd", return_value=tmp_path),
|
||||
patch("crewai.experimental.skills.registry.SkillCacheManager", return_value=mock_cache),
|
||||
patch("crewai.skills.registry.SkillCacheManager", return_value=mock_cache),
|
||||
):
|
||||
from crewai.experimental.skills.registry import resolve_registry_ref
|
||||
from crewai.skills.registry import resolve_registry_ref
|
||||
with pytest.raises(SkillNotCachedError) as exc_info:
|
||||
resolve_registry_ref("@acme/ghost-skill")
|
||||
assert "@acme/ghost-skill" in str(exc_info.value)
|
||||
@@ -112,11 +112,11 @@ class TestResolveRegistryRef:
|
||||
|
||||
# tmp_path has no ./skills/ directory
|
||||
with (
|
||||
patch("crewai.experimental.skills.registry._is_noninteractive", return_value=False),
|
||||
patch("crewai.skills.registry._is_noninteractive", return_value=False),
|
||||
patch.object(Path, "cwd", return_value=tmp_path),
|
||||
patch("crewai.experimental.skills.registry.SkillCacheManager", return_value=mock_cache),
|
||||
patch("crewai.skills.registry.SkillCacheManager", return_value=mock_cache),
|
||||
):
|
||||
from crewai.experimental.skills.registry import resolve_registry_ref
|
||||
from crewai.skills.registry import resolve_registry_ref
|
||||
skill = resolve_registry_ref("@acme/cached-skill")
|
||||
|
||||
assert skill.name == "cached-skill"
|
||||
Reference in New Issue
Block a user