refactor(core): extract crewai-core for shared utilities and standalone CLI

- New lib/crewai-core/ package: version, paths, constants, lock_store, user_data,
  printer, telemetry. Pure leaf — depends only on appdirs/portalocker/rich/otel.
- crewai now depends on crewai-core; old crewai.utilities.{version,paths,printer,
  lock_store} and the user-data block of events/listeners/tracing/utils.py become
  one-shot DeprecationWarning shims that re-export from crewai_core.
- crewai-cli drops its hard dep on crewai and depends only on crewai-core. CLI
  imports for telemetry/version/printer/constants now point at crewai_core.
- tools/main.py lazy-imports project_utils + get_user_id; the publish/login
  subcommands print a friendly "requires crewai" error if it's missing.
- crewai-cli is now genuinely standalone: 'crewai --help', 'version', 'login',
  'config', 'traces', 'create', 'template' all work without crewai installed.
- 351 CLI tests + 9 crewai-core smoke tests + crewai's full mypy (471 files) clean.
This commit is contained in:
Greyson Lalonde
2026-05-05 12:41:28 +08:00
parent d1934dabc0
commit 60f3df793f
69 changed files with 960 additions and 443 deletions

View File

@@ -1,8 +1,7 @@
# crewai-cli
CLI for CrewAI — scaffold, run, deploy and manage AI agent crews.
The CLI depends on the `crewai` framework and pulls it in automatically.
CLI for CrewAI — scaffold, run, deploy and manage AI agent crews without
installing the full framework.
## Installation
@@ -10,7 +9,17 @@ The CLI depends on the `crewai` framework and pulls it in automatically.
pip install crewai-cli
```
Or install via the framework's extra:
This pulls in `crewai-core` (shared utilities) but not the `crewai` framework
itself, so commands that don't need a crew loaded — `crewai version`,
`crewai login`, `crewai org list`, `crewai config *`, `crewai traces *`,
`crewai create`, `crewai template *` — work standalone.
Commands that load a user's crew or flow (`crewai run`, `crewai train`,
`crewai test`, `crewai chat`, `crewai replay`, `crewai reset-memories`,
`crewai deploy push`, `crewai tool publish`) require `crewai` to be installed
in the project's environment. They print a clear error if it is missing.
To install both at once:
```bash
pip install crewai[cli]

View File

@@ -8,7 +8,7 @@ authors = [
]
requires-python = ">=3.10, <3.14"
dependencies = [
"crewai>=1.14.5a2",
"crewai-core>=1.14.5a2",
"click~=8.1.7",
"pydantic>=2.11.9,<2.13",
"pydantic-settings~=2.10.1",
@@ -22,7 +22,6 @@ dependencies = [
"packaging>=23.0",
"python-dotenv>=1.2.2,<2",
"uv~=0.9.13",
"portalocker~=2.7.0",
]
[project.urls]

View File

@@ -1,7 +1,7 @@
from pathlib import Path
import click
from crewai.utilities.printer import PRINTER
from crewai_core.printer import PRINTER
from crewai_cli.utils import copy_template

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import json
from crewai.telemetry.telemetry import Telemetry
from crewai_core.telemetry import Telemetry
import httpx
from rich.console import Console

View File

@@ -2,7 +2,7 @@ from pathlib import Path
import shutil
import click
from crewai.telemetry import Telemetry
from crewai_core.telemetry import Telemetry
def create_flow(name: str) -> None:

View File

@@ -1,7 +1,7 @@
import subprocess
import click
from crewai.utilities.constants import CREWAI_TRAINED_AGENTS_FILE_ENV
from crewai_core.constants import CREWAI_TRAINED_AGENTS_FILE_ENV
from crewai_cli.utils import build_env_with_all_tool_credentials

View File

@@ -1,7 +1,7 @@
import subprocess
import click
from crewai.utilities.constants import CREWAI_TRAINED_AGENTS_FILE_ENV
from crewai_core.constants import CREWAI_TRAINED_AGENTS_FILE_ENV
from crewai_cli.utils import build_env_with_all_tool_credentials

View File

@@ -2,7 +2,7 @@ from enum import Enum
import subprocess
import click
from crewai.utilities.constants import CREWAI_TRAINED_AGENTS_FILE_ENV
from crewai_core.constants import CREWAI_TRAINED_AGENTS_FILE_ENV
from packaging import version
from crewai_cli.utils import build_env_with_all_tool_credentials, read_toml

View File

@@ -8,11 +8,6 @@ import tempfile
from typing import Any
import click
from crewai.events.listeners.tracing.utils import get_user_id
from crewai.utilities.project_utils import (
extract_available_exports,
extract_tools_metadata,
)
from rich.console import Console
from crewai_cli import git
@@ -33,6 +28,32 @@ from crewai_cli.utils import (
console = Console()
_REQUIRES_CREWAI_MSG = (
"[red]This subcommand requires the full crewai package.\n"
"Install it with: pip install crewai[/red]"
)
def _require_project_utils() -> Any:
try:
from crewai.utilities import project_utils
return project_utils
except ImportError:
console.print(_REQUIRES_CREWAI_MSG)
raise SystemExit(1) from None
def _require_get_user_id() -> Any:
try:
from crewai.events.listeners.tracing.utils import get_user_id
return get_user_id
except ImportError:
console.print(_REQUIRES_CREWAI_MSG)
raise SystemExit(1) from None
class ToolCommand(BaseCommand, PlusAPIMixin):
"""
A class to handle tool repository related operations for CrewAI projects.
@@ -99,7 +120,8 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
encoded_tarball = None
console.print("[bold blue]Discovering tools from your project...[/bold blue]")
available_exports = extract_available_exports()
project_utils = _require_project_utils()
available_exports = project_utils.extract_available_exports()
if available_exports:
console.print(
@@ -108,7 +130,7 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
console.print("[bold blue]Extracting tool metadata...[/bold blue]")
try:
tools_metadata = extract_tools_metadata()
tools_metadata = project_utils.extract_tools_metadata()
except Exception as e:
console.print(
f"[yellow]Warning: Could not extract tool metadata: {e}[/yellow]\n"
@@ -202,6 +224,7 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
console.print(f"Successfully installed {handle}", style="bold green")
def login(self) -> None:
get_user_id = _require_get_user_id()
login_response = self.plus_api_client.login_to_tool_repository(
user_identifier=get_user_id()
)

View File

@@ -1,71 +1,22 @@
"""Standalone user-data helpers for the CLI package.
These mirror the functions in ``crewai.events.listeners.tracing.utils`` but
depend only on the standard library + *appdirs* so that crewai-cli can work
without importing the full crewai framework.
"""
"""User-data helpers — re-exported from ``crewai_core.user_data``."""
from __future__ import annotations
import json
import logging
import os
from pathlib import Path
from typing import Any, cast
import appdirs
from crewai_core.paths import db_storage_path as _db_storage_path
from crewai_core.user_data import (
_load_user_data as _load_user_data,
_save_user_data as _save_user_data,
has_user_declined_tracing as has_user_declined_tracing,
is_tracing_enabled as is_tracing_enabled,
update_user_data as update_user_data,
)
logger = logging.getLogger(__name__)
def _get_project_directory_name() -> str:
return os.environ.get("CREWAI_STORAGE_DIR", Path.cwd().name)
def _db_storage_path() -> str:
app_name = _get_project_directory_name()
app_author = "CrewAI"
data_dir = Path(appdirs.user_data_dir(app_name, app_author))
data_dir.mkdir(parents=True, exist_ok=True)
return str(data_dir)
def _user_data_file() -> Path:
base = Path(_db_storage_path())
base.mkdir(parents=True, exist_ok=True)
return base / ".crewai_user.json"
def _load_user_data() -> dict[str, Any]:
p = _user_data_file()
if p.exists():
try:
return cast(dict[str, Any], json.loads(p.read_text()))
except (json.JSONDecodeError, OSError, PermissionError) as e:
logger.warning("Failed to load user data: %s", e)
return {}
def _save_user_data(data: dict[str, Any]) -> None:
try:
p = _user_data_file()
p.write_text(json.dumps(data, indent=2))
except (OSError, PermissionError) as e:
logger.warning("Failed to save user data: %s", e)
def is_tracing_enabled() -> bool:
"""Check if tracing is enabled.
Mirrors ``crewai.events.listeners.tracing.utils.is_tracing_enabled``:
consent only *blocks* tracing; activation requires
``CREWAI_TRACING_ENABLED=true``.
"""
data = _load_user_data()
if (
data.get("first_execution_done", False)
and data.get("trace_consent", False) is False
):
return False
return os.getenv("CREWAI_TRACING_ENABLED", "false").lower() == "true"
__all__ = [
"_db_storage_path",
"_load_user_data",
"_save_user_data",
"has_user_declined_tracing",
"is_tracing_enabled",
"update_user_data",
]

View File

@@ -10,7 +10,7 @@ from urllib import request
from urllib.error import URLError
import appdirs
from crewai.utilities.version import get_crewai_version as get_crewai_version
from crewai_core.version import get_crewai_version as get_crewai_version
from packaging.version import InvalidVersion, Version, parse

View File

@@ -184,11 +184,11 @@ def test_publish_when_not_in_sync(mock_is_synced, mock_fetch, capsys, tool_comma
@patch("crewai_cli.plus_api.PlusAPI.publish_tool")
@patch("crewai_cli.tools.main.git.Repository.is_synced", return_value=False)
@patch(
"crewai_cli.tools.main.extract_available_exports",
"crewai.utilities.project_utils.extract_available_exports",
return_value=[{"name": "SampleTool"}],
)
@patch(
"crewai_cli.tools.main.extract_tools_metadata",
"crewai.utilities.project_utils.extract_tools_metadata",
return_value=[{"name": "SampleTool", "humanized_name": "sample_tool", "description": "A sample tool", "run_params_schema": {}, "init_params_schema": {}, "env_vars": []}],
)
@patch("crewai_cli.tools.main.ToolCommand._print_current_organization")
@@ -250,11 +250,11 @@ def test_publish_when_not_in_sync_and_force(
@patch("crewai_cli.plus_api.PlusAPI.publish_tool")
@patch("crewai_cli.tools.main.git.Repository.is_synced", return_value=True)
@patch(
"crewai_cli.tools.main.extract_available_exports",
"crewai.utilities.project_utils.extract_available_exports",
return_value=[{"name": "SampleTool"}],
)
@patch(
"crewai_cli.tools.main.extract_tools_metadata",
"crewai.utilities.project_utils.extract_tools_metadata",
return_value=[{"name": "SampleTool", "humanized_name": "sample_tool", "description": "A sample tool", "run_params_schema": {}, "init_params_schema": {}, "env_vars": []}],
)
def test_publish_success(
@@ -311,11 +311,11 @@ def test_publish_success(
)
@patch("crewai_cli.plus_api.PlusAPI.publish_tool")
@patch(
"crewai_cli.tools.main.extract_available_exports",
"crewai.utilities.project_utils.extract_available_exports",
return_value=[{"name": "SampleTool"}],
)
@patch(
"crewai_cli.tools.main.extract_tools_metadata",
"crewai.utilities.project_utils.extract_tools_metadata",
return_value=[{"name": "SampleTool", "humanized_name": "sample_tool", "description": "A sample tool", "run_params_schema": {}, "init_params_schema": {}, "env_vars": []}],
)
def test_publish_failure(
@@ -357,11 +357,11 @@ def test_publish_failure(
)
@patch("crewai_cli.plus_api.PlusAPI.publish_tool")
@patch(
"crewai_cli.tools.main.extract_available_exports",
"crewai.utilities.project_utils.extract_available_exports",
return_value=[{"name": "SampleTool"}],
)
@patch(
"crewai_cli.tools.main.extract_tools_metadata",
"crewai.utilities.project_utils.extract_tools_metadata",
return_value=[{"name": "SampleTool", "humanized_name": "sample_tool", "description": "A sample tool", "run_params_schema": {}, "init_params_schema": {}, "env_vars": []}],
)
def test_publish_api_error(
@@ -404,11 +404,11 @@ def test_publish_api_error(
@patch("crewai_cli.plus_api.PlusAPI.publish_tool")
@patch("crewai_cli.tools.main.git.Repository.is_synced", return_value=True)
@patch(
"crewai_cli.tools.main.extract_available_exports",
"crewai.utilities.project_utils.extract_available_exports",
return_value=[{"name": "SampleTool"}],
)
@patch(
"crewai_cli.tools.main.extract_tools_metadata",
"crewai.utilities.project_utils.extract_tools_metadata",
side_effect=Exception("Failed to extract metadata"),
)
def test_publish_metadata_extraction_failure_continues_with_warning(