From 34bed359a6b060034099203393e6fd0777e4edc5 Mon Sep 17 00:00:00 2001 From: Heitor Carvalho Date: Tue, 23 Sep 2025 11:55:15 -0300 Subject: [PATCH] feat: add `crewai uv` wrapper for `uv` commands (#3581) --- docs/en/concepts/cli.mdx | 4 ++ .../enterprise/features/tool-repository.mdx | 30 +++++++++++ src/crewai/cli/cli.py | 53 +++++++++++++++++-- src/crewai/cli/tools/main.py | 34 ++++-------- src/crewai/cli/utils.py | 16 ++++++ 5 files changed, 108 insertions(+), 29 deletions(-) diff --git a/docs/en/concepts/cli.mdx b/docs/en/concepts/cli.mdx index 2935b52a4..c6724fc06 100644 --- a/docs/en/concepts/cli.mdx +++ b/docs/en/concepts/cli.mdx @@ -404,6 +404,10 @@ crewai config reset After resetting configuration, re-run `crewai login` to authenticate again. + +CrewAI CLI handles authentication to the Tool Repository automatically when adding packages to your project. Just append `crewai` before any `uv` command to use it. E.g. `crewai uv add requests`. For more information, see [Tool Repository](https://docs.crewai.com/enterprise/features/tool-repository) docs. + + Configuration settings are stored in `~/.config/crewai/settings.json`. Some settings like organization name and UUID are read-only and managed through authentication and organization commands. Tool repository related settings are hidden and cannot be set directly by users. diff --git a/docs/en/enterprise/features/tool-repository.mdx b/docs/en/enterprise/features/tool-repository.mdx index 9a578e117..7ec7ff346 100644 --- a/docs/en/enterprise/features/tool-repository.mdx +++ b/docs/en/enterprise/features/tool-repository.mdx @@ -52,6 +52,36 @@ researcher = Agent( ) ``` +## Adding other packages after installing a tool + +After installing a tool from the CrewAI Enterprise Tool Repository, you need to use the `crewai uv` command to add other packages to your project. +Using pure `uv` commands will fail due to authentication to tool repository being handled by the CLI. By using the `crewai uv` command, you can add other packages to your project without having to worry about authentication. +Any `uv` command can be used with the `crewai uv` command, making it a powerful tool for managing your project's dependencies without the hassle of managing authentication through environment variables or other methods. + +Say that you have installed a custom tool from the CrewAI Enterprise Tool Repository called "my-tool": + +```bash +crewai tool install my-tool +``` + +And now you want to add another package to your project, you can use the following command: + +```bash +crewai uv add requests +``` + +Other commands like `uv sync` or `uv remove` can also be used with the `crewai uv` command: + +```bash +crewai uv sync +``` + +```bash +crewai uv remove requests +``` + +This will add the package to your project and update `pyproject.toml` accordingly. + ## Creating and Publishing Tools To create a new tool project: diff --git a/src/crewai/cli/cli.py b/src/crewai/cli/cli.py index b9bf7147b..991082de0 100644 --- a/src/crewai/cli/cli.py +++ b/src/crewai/cli/cli.py @@ -1,3 +1,5 @@ +import os +import subprocess from importlib.metadata import version as get_version import click @@ -8,6 +10,7 @@ from crewai.cli.create_crew import create_crew from crewai.cli.create_flow import create_flow from crewai.cli.crew_chat import run_chat from crewai.cli.settings.main import SettingsCommand +from crewai.cli.utils import build_env_with_tool_repository_credentials, read_toml from crewai.memory.storage.kickoff_task_outputs_storage import ( KickoffTaskOutputsSQLiteStorage, ) @@ -34,6 +37,46 @@ def crewai(): """Top-level command group for crewai.""" +@crewai.command( + name="uv", + context_settings=dict( + ignore_unknown_options=True, + ), +) +@click.argument("uv_args", nargs=-1, type=click.UNPROCESSED) +def uv(uv_args): + """A wrapper around uv commands that adds custom tool authentication through env vars.""" + env = os.environ.copy() + try: + pyproject_data = read_toml() + sources = pyproject_data.get("tool", {}).get("uv", {}).get("sources", {}) + + for source_config in sources.values(): + if isinstance(source_config, dict): + index = source_config.get("index") + if index: + index_env = build_env_with_tool_repository_credentials(index) + env.update(index_env) + except (FileNotFoundError, KeyError) as e: + raise SystemExit( + "Error. A valid pyproject.toml file is required. Check that a valid pyproject.toml file exists in the current directory." + ) from e + except Exception as e: + raise SystemExit(f"Error: {e}") from e + + try: + subprocess.run( # noqa: S603 + ["uv", *uv_args], # noqa: S607 + capture_output=False, + env=env, + text=True, + check=True, + ) + except subprocess.CalledProcessError as e: + click.secho(f"uv command failed with exit code {e.returncode}", fg="red") + raise SystemExit(e.returncode) from e + + @crewai.command() @click.argument("type", type=click.Choice(["crew", "flow"])) @click.argument("name") @@ -239,11 +282,6 @@ def deploy(): """Deploy the Crew CLI group.""" -@crewai.group() -def tool(): - """Tool Repository related commands.""" - - @deploy.command(name="create") @click.option("-y", "--yes", is_flag=True, help="Skip the confirmation prompt") def deploy_create(yes: bool): @@ -291,6 +329,11 @@ def deploy_remove(uuid: str | None): deploy_cmd.remove_crew(uuid=uuid) +@crewai.group() +def tool(): + """Tool Repository related commands.""" + + @tool.command(name="create") @click.argument("handle") def tool_create(handle: str): diff --git a/src/crewai/cli/tools/main.py b/src/crewai/cli/tools/main.py index 25cf89ee8..a7fc718c7 100644 --- a/src/crewai/cli/tools/main.py +++ b/src/crewai/cli/tools/main.py @@ -12,6 +12,7 @@ from crewai.cli import git from crewai.cli.command import BaseCommand, PlusAPIMixin from crewai.cli.config import Settings from crewai.cli.utils import ( + build_env_with_tool_repository_credentials, extract_available_exports, get_project_description, get_project_name, @@ -42,8 +43,7 @@ class ToolCommand(BaseCommand, PlusAPIMixin): if project_root.exists(): click.secho(f"Folder {folder_name} already exists.", fg="red") raise SystemExit - else: - os.makedirs(project_root) + os.makedirs(project_root) click.secho(f"Creating custom tool {folder_name}...", fg="green", bold=True) @@ -56,7 +56,7 @@ class ToolCommand(BaseCommand, PlusAPIMixin): os.chdir(project_root) try: self.login() - subprocess.run(["git", "init"], check=True) + subprocess.run(["git", "init"], check=True) # noqa: S607 console.print( f"[green]Created custom tool [bold]{folder_name}[/bold]. Run [bold]cd {project_root}[/bold] to start working.[/green]" ) @@ -76,10 +76,10 @@ class ToolCommand(BaseCommand, PlusAPIMixin): raise SystemExit() project_name = get_project_name(require=True) - assert isinstance(project_name, str) + assert isinstance(project_name, str) # noqa: S101 project_version = get_project_version(require=True) - assert isinstance(project_version, str) + assert isinstance(project_version, str) # noqa: S101 project_description = get_project_description(require=False) encoded_tarball = None @@ -94,8 +94,8 @@ class ToolCommand(BaseCommand, PlusAPIMixin): self._print_current_organization() with tempfile.TemporaryDirectory() as temp_build_dir: - subprocess.run( - ["uv", "build", "--sdist", "--out-dir", temp_build_dir], + subprocess.run( # noqa: S603 + ["uv", "build", "--sdist", "--out-dir", temp_build_dir], # noqa: S607 check=True, capture_output=False, ) @@ -146,7 +146,7 @@ class ToolCommand(BaseCommand, PlusAPIMixin): style="bold red", ) raise SystemExit - elif get_response.status_code != 200: + if get_response.status_code != 200: console.print( "Failed to get tool details. Please try again later.", style="bold red" ) @@ -196,10 +196,10 @@ class ToolCommand(BaseCommand, PlusAPIMixin): else: add_package_command.extend(["--index", index, tool_handle]) - add_package_result = subprocess.run( + add_package_result = subprocess.run( # noqa: S603 add_package_command, capture_output=False, - env=self._build_env_with_credentials(repository_handle), + env=build_env_with_tool_repository_credentials(repository_handle), text=True, check=True, ) @@ -221,20 +221,6 @@ class ToolCommand(BaseCommand, PlusAPIMixin): ) raise SystemExit - def _build_env_with_credentials(self, repository_handle: str): - repository_handle = repository_handle.upper().replace("-", "_") - settings = Settings() - - env = os.environ.copy() - env[f"UV_INDEX_{repository_handle}_USERNAME"] = str( - settings.tool_repository_username or "" - ) - env[f"UV_INDEX_{repository_handle}_PASSWORD"] = str( - settings.tool_repository_password or "" - ) - - return env - def _print_current_organization(self) -> None: settings = Settings() if settings.org_uuid: diff --git a/src/crewai/cli/utils.py b/src/crewai/cli/utils.py index 764af9d2f..fc0ad7ab3 100644 --- a/src/crewai/cli/utils.py +++ b/src/crewai/cli/utils.py @@ -11,6 +11,7 @@ import click import tomli from rich.console import Console +from crewai.cli.config import Settings from crewai.cli.constants import ENV_VARS from crewai.crew import Crew from crewai.flow import Flow @@ -417,6 +418,21 @@ def extract_available_exports(dir_path: str = "src"): raise SystemExit(1) from e +def build_env_with_tool_repository_credentials(repository_handle: str): + repository_handle = repository_handle.upper().replace("-", "_") + settings = Settings() + + env = os.environ.copy() + env[f"UV_INDEX_{repository_handle}_USERNAME"] = str( + settings.tool_repository_username or "" + ) + env[f"UV_INDEX_{repository_handle}_PASSWORD"] = str( + settings.tool_repository_password or "" + ) + + return env + + def _load_tools_from_init(init_file: Path) -> list[dict[str, Any]]: """ Load and validate tools from a given __init__.py file.