Compare commits

...

3 Commits

Author SHA1 Message Date
Greyson LaLonde
fa6287327d docs: update changelog and version for v1.14.5a3 2026-05-07 01:58:27 +08:00
Greyson LaLonde
e961a005cb feat: bump versions to 1.14.5a3
Some checks failed
Build uv cache / build-cache (3.10) (push) Has been cancelled
Build uv cache / build-cache (3.11) (push) Has been cancelled
Build uv cache / build-cache (3.12) (push) Has been cancelled
Build uv cache / build-cache (3.13) (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
2026-05-07 01:44:05 +08:00
Greyson LaLonde
93e786d263 refactor: extract CLI into standalone crewai-cli package 2026-05-06 20:46:46 +08:00
258 changed files with 5780 additions and 1828 deletions

View File

@@ -19,7 +19,7 @@ repos:
language: system
pass_filenames: true
types: [python]
exclude: ^(lib/crewai/src/crewai/cli/templates/|lib/crewai/tests/|lib/crewai-tools/tests/|lib/crewai-files/tests/)
exclude: ^(lib/crewai/src/crewai/cli/templates/|lib/cli/src/crewai_cli/templates/|lib/cli/tests/|lib/crewai/tests/|lib/crewai-tools/tests/|lib/crewai-files/tests/|lib/devtools/tests/)
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.11.3
hooks:

View File

@@ -54,12 +54,13 @@ _original_from_serialized_response = getattr(
)
if _original_from_serialized_response is not None:
_from_serialized: Any = _original_from_serialized_response
def _patched_from_serialized_response(
request: Any, serialized_response: Any, history: Any = None
) -> Any:
"""Patched version that ensures response._content is properly set."""
response = _original_from_serialized_response(request, serialized_response, history)
response = _from_serialized(request, serialized_response, history)
# Explicitly set _content to avoid ResponseNotRead errors
# The content was passed to the constructor but the mocked read() prevents
# proper initialization of the internal state
@@ -255,7 +256,8 @@ def vcr_cassette_dir(request: Any) -> str:
for parent in test_file.parents:
if (
parent.name in ("crewai", "crewai-tools", "crewai-files")
parent.name
in ("crewai", "crewai-tools", "crewai-files", "cli", "crewai-core")
and parent.parent.name == "lib"
):
package_root = parent

View File

@@ -4,6 +4,29 @@ description: "تحديثات المنتج والتحسينات وإصلاحات
icon: "clock"
mode: "wide"
---
<Update label="7 مايو 2026">
## v1.14.5a3
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5a3)
## ما الذي تغير
### إصلاحات الأخطاء
- إصلاح مسار نقطة النهاية للحالة من /{kickoff_id}/status إلى /status/{kickoff_id}
- تحديث تبعية gitpython إلى الإصدار >=3.1.47 للامتثال الأمني
### إعادة هيكلة
- استخراج واجهة سطر الأوامر إلى حزمة crewai-cli المستقلة
### الوثائق
- تحديث سجل التغييرات والإصدار للإصدار v1.14.5a2
## المساهمون
@greysonlalonde, @iris-clawd
</Update>
<Update label="4 مايو 2026">
## v1.14.5a2

View File

@@ -4,6 +4,29 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
icon: "clock"
mode: "wide"
---
<Update label="May 07, 2026">
## v1.14.5a3
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5a3)
## What's Changed
### Bug Fixes
- Fix status endpoint path from /{kickoff_id}/status to /status/{kickoff_id}
- Bump gitpython dependency to version >=3.1.47 for security compliance
### Refactoring
- Extract CLI into standalone crewai-cli package
### Documentation
- Update changelog and version for v1.14.5a2
## Contributors
@greysonlalonde, @iris-clawd
</Update>
<Update label="May 04, 2026">
## v1.14.5a2

View File

@@ -4,6 +4,29 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
icon: "clock"
mode: "wide"
---
<Update label="2026년 5월 7일">
## v1.14.5a3
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5a3)
## 변경 사항
### 버그 수정
- 상태 엔드포인트 경로를 /{kickoff_id}/status에서 /status/{kickoff_id}로 수정
- 보안 준수를 위해 gitpython 의존성을 버전 >=3.1.47로 업데이트
### 리팩토링
- CLI를 독립형 crewai-cli 패키지로 분리
### 문서
- v1.14.5a2에 대한 변경 로그 및 버전 업데이트
## 기여자
@greysonlalonde, @iris-clawd
</Update>
<Update label="2026년 5월 4일">
## v1.14.5a2

View File

@@ -4,6 +4,29 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
icon: "clock"
mode: "wide"
---
<Update label="07 mai 2026">
## v1.14.5a3
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5a3)
## O que Mudou
### Correções de Bugs
- Corrigir o caminho do endpoint de status de /{kickoff_id}/status para /status/{kickoff_id}
- Atualizar a dependência gitpython para a versão >=3.1.47 para conformidade de segurança
### Refatoração
- Extrair CLI para o pacote independente crewai-cli
### Documentação
- Atualizar o changelog e a versão para v1.14.5a2
## Contributors
@greysonlalonde, @iris-clawd
</Update>
<Update label="04 mai 2026">
## v1.14.5a2

26
lib/cli/README.md Normal file
View File

@@ -0,0 +1,26 @@
# crewai-cli
CLI for CrewAI — scaffold, run, deploy and manage AI agent crews without
installing the full framework.
## Installation
```bash
pip install crewai-cli
```
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]
```

43
lib/cli/pyproject.toml Normal file
View File

@@ -0,0 +1,43 @@
[project]
name = "crewai-cli"
dynamic = ["version"]
description = "CLI for CrewAI — scaffold, run, deploy and manage AI agent crews."
readme = "README.md"
authors = [
{ name = "Joao Moura", email = "joao@crewai.com" }
]
requires-python = ">=3.10, <3.14"
dependencies = [
"crewai-core==1.14.5a3",
"click~=8.1.7",
"pydantic>=2.11.9,<2.13",
"pydantic-settings~=2.10.1",
"appdirs~=1.4.4",
"cryptography>=42.0",
"httpx~=0.28.1",
"pyjwt>=2.9.0,<3",
"rich>=13.7.1",
"tomli~=2.0.2",
"tomli-w~=1.1.0",
"packaging>=23.0",
"python-dotenv>=1.2.2,<2",
"uv~=0.11.6",
]
[project.urls]
Homepage = "https://crewai.com"
Documentation = "https://docs.crewai.com"
Repository = "https://github.com/crewAIInc/crewAI"
[project.scripts]
crewai = "crewai_cli.cli:crewai"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.version]
path = "src/crewai_cli/__init__.py"
[tool.hatch.build.targets.wheel]
packages = ["src/crewai_cli"]

View File

@@ -0,0 +1 @@
__version__ = "1.14.5a3"

View File

@@ -1,9 +1,9 @@
from pathlib import Path
import click
from crewai_core.printer import PRINTER
from crewai.cli.utils import copy_template
from crewai.utilities.printer import PRINTER
from crewai_cli.utils import copy_template
def add_crew_to_flow(crew_name: str) -> None:

View File

@@ -0,0 +1,8 @@
"""CLI authentication entry point."""
from __future__ import annotations
from crewai_cli.authentication.main import AuthenticationCommand
__all__ = ["AuthenticationCommand"]

View File

@@ -0,0 +1,8 @@
"""Re-export of authentication constants from ``crewai_core.auth.constants``."""
from __future__ import annotations
from crewai_core.auth.constants import ALGORITHMS as ALGORITHMS
__all__ = ["ALGORITHMS"]

View File

@@ -0,0 +1,60 @@
"""CLI-side authentication wiring.
Re-exports the OAuth2 primitives from ``crewai_core.auth`` and overrides the
``_post_login`` hook to also log into the tool repository.
"""
from __future__ import annotations
from crewai_core.auth.oauth2 import (
AuthenticationCommand as _BaseAuthenticationCommand,
Oauth2Settings as Oauth2Settings,
ProviderFactory as ProviderFactory,
console,
)
from crewai_core.settings import Settings
__all__ = ["AuthenticationCommand", "Oauth2Settings", "ProviderFactory"]
class AuthenticationCommand(_BaseAuthenticationCommand):
"""CLI-side login that also signs the user into the tool repository."""
def _post_login(self) -> None:
self._login_to_tool_repository()
def _login_to_tool_repository(self) -> None:
from crewai_cli.tools.main import ToolCommand
try:
console.print(
"Now logging you in to the Tool Repository... ",
style="bold blue",
end="",
)
ToolCommand().login()
console.print(
"Success!\n",
style="bold green",
)
settings = Settings()
console.print(
f"You are now authenticated to the tool repository for organization [bold cyan]'{settings.org_name if settings.org_name else settings.org_uuid}'[/bold cyan]",
style="green",
)
except (Exception, SystemExit):
console.print(
"\n[bold yellow]Warning:[/bold yellow] Authentication with the Tool Repository failed.",
style="yellow",
)
console.print(
"Other features will work normally, but you may experience limitations "
"with downloading and publishing tools."
"\nRun [bold]crewai login[/bold] to try logging in again.\n",
style="yellow",
)

View File

@@ -0,0 +1 @@
"""OAuth2 authentication providers — re-exported from ``crewai_core.auth.providers``."""

View File

@@ -0,0 +1,8 @@
"""Re-export of ``Auth0Provider`` from ``crewai_core.auth.providers.auth0``."""
from __future__ import annotations
from crewai_core.auth.providers.auth0 import Auth0Provider as Auth0Provider
__all__ = ["Auth0Provider"]

View File

@@ -0,0 +1,8 @@
"""Re-export of ``BaseProvider`` from ``crewai_core.auth.providers.base_provider``."""
from __future__ import annotations
from crewai_core.auth.providers.base_provider import BaseProvider as BaseProvider
__all__ = ["BaseProvider"]

View File

@@ -0,0 +1,8 @@
"""Re-export of ``EntraIdProvider`` from ``crewai_core.auth.providers.entra_id``."""
from __future__ import annotations
from crewai_core.auth.providers.entra_id import EntraIdProvider as EntraIdProvider
__all__ = ["EntraIdProvider"]

View File

@@ -0,0 +1,8 @@
"""Re-export of ``KeycloakProvider`` from ``crewai_core.auth.providers.keycloak``."""
from __future__ import annotations
from crewai_core.auth.providers.keycloak import KeycloakProvider as KeycloakProvider
__all__ = ["KeycloakProvider"]

View File

@@ -0,0 +1,8 @@
"""Re-export of ``OktaProvider`` from ``crewai_core.auth.providers.okta``."""
from __future__ import annotations
from crewai_core.auth.providers.okta import OktaProvider as OktaProvider
__all__ = ["OktaProvider"]

View File

@@ -0,0 +1,8 @@
"""Re-export of ``WorkosProvider`` from ``crewai_core.auth.providers.workos``."""
from __future__ import annotations
from crewai_core.auth.providers.workos import WorkosProvider as WorkosProvider
__all__ = ["WorkosProvider"]

View File

@@ -0,0 +1,11 @@
"""Re-exports of authentication token helpers from ``crewai_core.auth.token``."""
from __future__ import annotations
from crewai_core.auth.token import (
AuthError as AuthError,
get_auth_token as get_auth_token,
)
__all__ = ["AuthError", "get_auth_token"]

View File

@@ -0,0 +1,8 @@
"""Re-export of ``validate_jwt_token`` from ``crewai_core.auth.utils``."""
from __future__ import annotations
from crewai_core.auth.utils import validate_jwt_token as validate_jwt_token
__all__ = ["validate_jwt_token"]

View File

@@ -21,7 +21,7 @@ from textual.widgets import (
Tree,
)
from crewai.cli.checkpoint_cli import (
from crewai_cli.checkpoint_cli import (
_format_size,
_is_sqlite,
_list_json,

View File

@@ -1,50 +1,66 @@
from __future__ import annotations
from importlib.metadata import version as get_version
import os
import subprocess
from typing import Any
import click
from crewai_core.token_manager import TokenManager
from crewai.cli.add_crew_to_flow import add_crew_to_flow
from crewai.cli.authentication.main import AuthenticationCommand
from crewai.cli.config import Settings
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.deploy.main import DeployCommand
from crewai.cli.enterprise.main import EnterpriseConfigureCommand
from crewai.cli.evaluate_crew import evaluate_crew
from crewai.cli.install_crew import install_crew
from crewai.cli.kickoff_flow import kickoff_flow
from crewai.cli.organization.main import OrganizationCommand
from crewai.cli.plot_flow import plot_flow
from crewai.cli.remote_template.main import TemplateCommand
from crewai.cli.replay_from_task import replay_task_command
from crewai.cli.reset_memories_command import reset_memories_command
from crewai.cli.run_crew import run_crew
from crewai.cli.settings.main import SettingsCommand
from crewai.cli.shared.token_manager import TokenManager
from crewai.cli.tools.main import ToolCommand
from crewai.cli.train_crew import train_crew
from crewai.cli.triggers.main import TriggersCommand
from crewai.cli.update_crew import update_crew
from crewai.cli.utils import build_env_with_all_tool_credentials, read_toml
from crewai.memory.storage.kickoff_task_outputs_storage import (
KickoffTaskOutputsSQLiteStorage,
from crewai_cli.add_crew_to_flow import add_crew_to_flow
from crewai_cli.authentication.main import AuthenticationCommand
from crewai_cli.config import Settings
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.deploy.main import DeployCommand
from crewai_cli.enterprise.main import EnterpriseConfigureCommand
from crewai_cli.evaluate_crew import evaluate_crew
from crewai_cli.install_crew import install_crew
from crewai_cli.kickoff_flow import kickoff_flow
from crewai_cli.organization.main import OrganizationCommand
from crewai_cli.plot_flow import plot_flow
from crewai_cli.remote_template.main import TemplateCommand
from crewai_cli.replay_from_task import replay_task_command
from crewai_cli.reset_memories_command import reset_memories_command
from crewai_cli.run_crew import run_crew
from crewai_cli.settings.main import SettingsCommand
from crewai_cli.task_outputs import load_task_outputs
from crewai_cli.tools.main import ToolCommand
from crewai_cli.train_crew import train_crew
from crewai_cli.triggers.main import TriggersCommand
from crewai_cli.update_crew import update_crew
from crewai_cli.user_data import (
_load_user_data,
is_tracing_enabled,
update_user_data,
)
from crewai_cli.utils import build_env_with_all_tool_credentials, read_toml
def _get_cli_version() -> str:
"""Return the best available version string for the CLI."""
# Prefer crewai version if installed (keeps existing UX)
try:
return get_version("crewai")
except Exception: # noqa: S110
pass
try:
return get_version("crewai-cli")
except Exception:
return "unknown"
@click.group()
@click.version_option(get_version("crewai"))
@click.version_option(_get_cli_version())
def crewai() -> None:
"""Top-level command group for crewai."""
@crewai.command(
name="uv",
context_settings=dict(
ignore_unknown_options=True,
),
context_settings={"ignore_unknown_options": True},
)
@click.argument("uv_args", nargs=-1, type=click.UNPROCESSED)
def uv(uv_args: tuple[str, ...]) -> None:
@@ -105,7 +121,7 @@ def version(tools: bool) -> None:
if tools:
try:
tools_version = get_version("crewai")
tools_version = get_version("crewai-tools")
click.echo(f"crewai tools version: {tools_version}")
except Exception:
click.echo("crewai tools not installed")
@@ -168,12 +184,9 @@ def replay(task_id: str, trained_agents_file: str | None) -> None:
@crewai.command()
def log_tasks_outputs() -> None:
"""
Retrieve your latest crew.kickoff() task outputs.
"""
"""Retrieve your latest crew.kickoff() task outputs."""
try:
storage = KickoffTaskOutputsSQLiteStorage()
tasks = storage.load()
tasks = load_task_outputs()
if not tasks:
click.echo(
@@ -231,11 +244,8 @@ def reset_memories(
agent_knowledge: bool,
all: bool,
) -> None:
"""
Reset the crew memories (memory, knowledge, agent_knowledge, kickoff_outputs). This will delete all the data saved.
"""
"""Reset the crew memories (memory, knowledge, agent_knowledge, kickoff_outputs). This will delete all the data saved."""
try:
# Treat legacy flags as --memory with a deprecation warning
if long or short or entities:
legacy_used = [
f
@@ -302,7 +312,7 @@ def memory(
) -> None:
"""Open the Memory TUI to browse scopes and recall memories."""
try:
from crewai.cli.memory_tui import MemoryTUI
from crewai_cli.memory_tui import MemoryTUI
except ImportError as exc:
click.echo(
"Textual is required for the memory TUI but could not be imported. "
@@ -365,10 +375,10 @@ def test(n_iterations: int, model: str, trained_agents_file: str | None) -> None
@crewai.command(
context_settings=dict(
ignore_unknown_options=True,
allow_extra_args=True,
)
context_settings={
"ignore_unknown_options": True,
"allow_extra_args": True,
}
)
@click.pass_context
def install(context: click.Context) -> None:
@@ -471,7 +481,7 @@ def deploy_validate() -> None:
`crewai deploy push` run automatically, without contacting the platform.
Exits non-zero if any blocking issues are found.
"""
from crewai.cli.deploy.validate import run_validate_command
from crewai_cli.deploy.validate import run_validate_command
run_validate_command()
@@ -612,14 +622,12 @@ def triggers_run(trigger_path: str) -> None:
@crewai.command()
def chat() -> None:
"""
Start a conversation with the Crew, collecting user-supplied inputs,
"""Start a conversation with the Crew, collecting user-supplied inputs,
and using the Chat LLM to generate responses.
"""
click.secho(
"\nStarting a conversation with the Crew\nType 'exit' or Ctrl+C to quit.\n",
)
run_chat()
@@ -784,16 +792,14 @@ def traces_enable() -> None:
from rich.console import Console
from rich.panel import Panel
from crewai.events.listeners.tracing.utils import update_user_data
console = Console()
update_user_data({"trace_consent": True, "first_execution_done": True})
panel = Panel(
"✅ Trace collection has been enabled!\n\n"
"✅ Trace collection enabled.\n\n"
"Your crew/flow executions will now send traces to CrewAI+.\n"
"Use 'crewai traces disable' to turn off trace collection.",
"Use 'crewai traces disable' to opt out.",
title="Traces Enabled",
border_style="green",
padding=(1, 2),
@@ -807,16 +813,16 @@ def traces_disable() -> None:
from rich.console import Console
from rich.panel import Panel
from crewai.events.listeners.tracing.utils import update_user_data
console = Console()
update_user_data({"trace_consent": False, "first_execution_done": True})
panel = Panel(
"❌ Trace collection has been disabled!\n\n"
"Your crew/flow executions will no longer send traces.\n"
"Use 'crewai traces enable' to turn trace collection back on.",
"❌ Trace collection disabled.\n\n"
"Your crew/flow executions will no longer send traces "
"(unless [bold]CREWAI_TRACING_ENABLED=true[/bold] is set in the environment, "
"which overrides the opt-out).\n"
"Use 'crewai traces enable' to opt back in.",
title="Traces Disabled",
border_style="red",
padding=(1, 2),
@@ -832,11 +838,6 @@ def traces_status() -> None:
from rich.panel import Panel
from rich.table import Table
from crewai.events.listeners.tracing.utils import (
_load_user_data,
is_tracing_enabled,
)
console = Console()
user_data = _load_user_data()
@@ -883,13 +884,13 @@ def traces_status() -> None:
@click.pass_context
def checkpoint(ctx: click.Context, location: str) -> None:
"""Browse and inspect checkpoints. Launches a TUI when called without a subcommand."""
from crewai.cli.checkpoint_cli import _detect_location
from crewai_cli.checkpoint_cli import _detect_location
location = _detect_location(location)
ctx.ensure_object(dict)
ctx.obj["location"] = location
if ctx.invoked_subcommand is None:
from crewai.cli.checkpoint_tui import run_checkpoint_tui
from crewai_cli.checkpoint_tui import run_checkpoint_tui
run_checkpoint_tui(location)
@@ -898,7 +899,7 @@ def checkpoint(ctx: click.Context, location: str) -> None:
@click.argument("location", default="./.checkpoints")
def checkpoint_list(location: str) -> None:
"""List checkpoints in a directory."""
from crewai.cli.checkpoint_cli import _detect_location, list_checkpoints
from crewai_cli.checkpoint_cli import _detect_location, list_checkpoints
list_checkpoints(_detect_location(location))
@@ -907,7 +908,7 @@ def checkpoint_list(location: str) -> None:
@click.argument("path", default="./.checkpoints")
def checkpoint_info(path: str) -> None:
"""Show details of a checkpoint. Pass a file or directory for latest."""
from crewai.cli.checkpoint_cli import _detect_location, info_checkpoint
from crewai_cli.checkpoint_cli import _detect_location, info_checkpoint
info_checkpoint(_detect_location(path))
@@ -917,7 +918,7 @@ def checkpoint_info(path: str) -> None:
@click.pass_context
def checkpoint_resume(ctx: click.Context, checkpoint_id: str | None) -> None:
"""Resume from a checkpoint. Defaults to the most recent."""
from crewai.cli.checkpoint_cli import resume_checkpoint
from crewai_cli.checkpoint_cli import resume_checkpoint
resume_checkpoint(ctx.obj["location"], checkpoint_id)
@@ -928,7 +929,7 @@ def checkpoint_resume(ctx: click.Context, checkpoint_id: str | None) -> None:
@click.pass_context
def checkpoint_diff(ctx: click.Context, id1: str, id2: str) -> None:
"""Compare two checkpoints side-by-side."""
from crewai.cli.checkpoint_cli import diff_checkpoints
from crewai_cli.checkpoint_cli import diff_checkpoints
diff_checkpoints(ctx.obj["location"], id1, id2)
@@ -950,7 +951,7 @@ def checkpoint_prune(
ctx: click.Context, keep: int | None, older_than: str | None, dry_run: bool
) -> None:
"""Remove old checkpoints."""
from crewai.cli.checkpoint_cli import prune_checkpoints
from crewai_cli.checkpoint_cli import prune_checkpoints
prune_checkpoints(ctx.obj["location"], keep, older_than, dry_run)

View File

@@ -1,11 +1,13 @@
from __future__ import annotations
import json
from crewai_core.telemetry import Telemetry
import httpx
from rich.console import Console
from crewai.cli.authentication.token import get_auth_token
from crewai.cli.plus_api import PlusAPI
from crewai.telemetry.telemetry import Telemetry
from crewai_cli.authentication.token import get_auth_token
from crewai_cli.plus_api import PlusAPI
console = Console()
@@ -32,11 +34,10 @@ class PlusAPIMixin:
raise SystemExit from None
def _validate_response(self, response: httpx.Response) -> None:
"""
Handle and display error messages from API responses.
"""Handle and display error messages from API responses.
Args:
response (httpx.Response): The response from the Plus API
response: The response from the Plus API.
"""
try:
json_response = response.json()

View File

@@ -0,0 +1,30 @@
"""Re-exports of shared settings from ``crewai_core.settings``.
Kept as a stable import path for the CLI; new code should import from
``crewai_core.settings`` directly.
"""
from __future__ import annotations
from crewai_core.settings import (
CLI_SETTINGS_KEYS as CLI_SETTINGS_KEYS,
DEFAULT_CLI_SETTINGS as DEFAULT_CLI_SETTINGS,
DEFAULT_CONFIG_PATH as DEFAULT_CONFIG_PATH,
HIDDEN_SETTINGS_KEYS as HIDDEN_SETTINGS_KEYS,
READONLY_SETTINGS_KEYS as READONLY_SETTINGS_KEYS,
USER_SETTINGS_KEYS as USER_SETTINGS_KEYS,
Settings as Settings,
get_writable_config_path as get_writable_config_path,
)
__all__ = [
"CLI_SETTINGS_KEYS",
"DEFAULT_CLI_SETTINGS",
"DEFAULT_CONFIG_PATH",
"HIDDEN_SETTINGS_KEYS",
"READONLY_SETTINGS_KEYS",
"USER_SETTINGS_KEYS",
"Settings",
"get_writable_config_path",
]

View File

@@ -5,13 +5,13 @@ import sys
import click
import tomli
from crewai.cli.constants import ENV_VARS, MODELS
from crewai.cli.provider import (
from crewai_cli.constants import ENV_VARS, MODELS
from crewai_cli.provider import (
get_provider_data,
select_model,
select_provider,
)
from crewai.cli.utils import copy_template, load_env_vars, write_env_file
from crewai_cli.utils import copy_template, load_env_vars, write_env_file
def get_reserved_script_names() -> set[str]:

View File

@@ -2,8 +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:
@@ -18,7 +17,6 @@ def create_flow(name: str) -> None:
click.secho(f"Error: Folder {folder_name} already exists.", fg="red")
return
# Initialize telemetry
telemetry = Telemetry()
telemetry.flow_creation_span(class_name)

View File

@@ -0,0 +1,23 @@
"""Wrapper for the crew chat command.
Delegates to ``crewai.utilities.crew_chat.run_chat`` when the full crewai
package is installed, otherwise prints a helpful error message.
"""
from __future__ import annotations
import click
def run_chat() -> None:
try:
from crewai.utilities.crew_chat import run_chat as _run_chat
except ImportError:
click.secho(
"The 'chat' command requires the full crewai package.\n"
"Install it with: pip install crewai",
fg="red",
)
raise SystemExit(1) from None
_run_chat()

View File

@@ -2,10 +2,10 @@ from typing import Any
from rich.console import Console
from crewai.cli import git
from crewai.cli.command import BaseCommand, PlusAPIMixin
from crewai.cli.deploy.validate import validate_project
from crewai.cli.utils import fetch_and_json_env_file, get_project_name
from crewai_cli import git
from crewai_cli.command import BaseCommand, PlusAPIMixin
from crewai_cli.deploy.validate import validate_project
from crewai_cli.utils import fetch_and_json_env_file, get_project_name
console = Console()

View File

@@ -40,7 +40,7 @@ from typing import Any
from rich.console import Console
from crewai.cli.utils import parse_toml
from crewai_cli.utils import parse_toml
console = Console()
@@ -438,7 +438,7 @@ class DeployValidator:
"import json, sys, traceback, os\n"
"os.chdir(sys.argv[1])\n"
"try:\n"
" from crewai.cli.utils import get_crews, get_flows\n"
" from crewai.utilities.project_utils import get_crews, get_flows\n"
" is_flow = sys.argv[2] == 'flow'\n"
" if is_flow:\n"
" instances = get_flows()\n"

View File

@@ -4,10 +4,10 @@ from typing import Any, cast
import httpx
from rich.console import Console
from crewai.cli.authentication.main import Oauth2Settings, ProviderFactory
from crewai.cli.command import BaseCommand
from crewai.cli.settings.main import SettingsCommand
from crewai.utilities.version import get_crewai_version
from crewai_cli.authentication.main import Oauth2Settings, ProviderFactory
from crewai_cli.command import BaseCommand
from crewai_cli.settings.main import SettingsCommand
from crewai_cli.version import get_crewai_version
console = Console()

View File

@@ -1,9 +1,9 @@
import subprocess
import click
from crewai_core.constants import CREWAI_TRAINED_AGENTS_FILE_ENV
from crewai.cli.utils import build_env_with_all_tool_credentials
from crewai.utilities.constants import CREWAI_TRAINED_AGENTS_FILE_ENV
from crewai_cli.utils import build_env_with_all_tool_credentials
def evaluate_crew(

View File

@@ -2,7 +2,7 @@ import subprocess
import click
from crewai.cli.utils import build_env_with_all_tool_credentials
from crewai_cli.utils import build_env_with_all_tool_credentials
# Be mindful about changing this.

View File

@@ -2,8 +2,8 @@ from httpx import HTTPStatusError
from rich.console import Console
from rich.table import Table
from crewai.cli.command import BaseCommand, PlusAPIMixin
from crewai.cli.config import Settings
from crewai_cli.command import BaseCommand, PlusAPIMixin
from crewai_cli.config import Settings
console = Console()

View File

@@ -0,0 +1,12 @@
"""Re-export of ``crewai_core.plus_api.PlusAPI``.
Kept as a stable import path for the CLI; new code should import from
``crewai_core.plus_api`` directly.
"""
from __future__ import annotations
from crewai_core.plus_api import PlusAPI as PlusAPI
__all__ = ["PlusAPI"]

View File

@@ -10,7 +10,7 @@ import certifi
import click
import httpx
from crewai.cli.constants import JSON_URL, MODELS, PROVIDERS
from crewai_cli.constants import JSON_URL, MODELS, PROVIDERS
def select_choice(prompt_message: str, choices: Sequence[str]) -> str | None:

View File

@@ -11,7 +11,7 @@ from rich.console import Console
from rich.panel import Panel
from rich.text import Text
from crewai.cli.command import BaseCommand
from crewai_cli.command import BaseCommand
logger = logging.getLogger(__name__)

View File

@@ -1,9 +1,9 @@
import subprocess
import click
from crewai_core.constants import CREWAI_TRAINED_AGENTS_FILE_ENV
from crewai.cli.utils import build_env_with_all_tool_credentials
from crewai.utilities.constants import CREWAI_TRAINED_AGENTS_FILE_ENV
from crewai_cli.utils import build_env_with_all_tool_credentials
def replay_task_command(task_id: str, trained_agents_file: str | None = None) -> None:

View File

@@ -0,0 +1,31 @@
"""Wrapper for the reset-memories command.
Delegates to ``crewai.utilities.reset_memories`` when the full crewai
package is installed, otherwise prints a helpful error message.
"""
from __future__ import annotations
import click
def reset_memories_command(
memory: bool,
knowledge: bool,
agent_knowledge: bool,
kickoff_outputs: bool,
all: bool,
) -> None:
try:
from crewai.utilities.reset_memories import (
reset_memories_command as _reset,
)
except ImportError:
click.secho(
"The 'reset-memories' command requires the full crewai package.\n"
"Install it with: pip install crewai",
fg="red",
)
raise SystemExit(1) from None
_reset(memory, knowledge, agent_knowledge, kickoff_outputs, all)

View File

@@ -2,11 +2,11 @@ from enum import Enum
import subprocess
import click
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
from crewai.utilities.constants import CREWAI_TRAINED_AGENTS_FILE_ENV
from crewai.utilities.version import get_crewai_version
from crewai_cli.utils import build_env_with_all_tool_credentials, read_toml
from crewai_cli.version import get_crewai_version
class CrewType(Enum):

View File

@@ -5,9 +5,9 @@ from typing import Any
from rich.console import Console
from rich.table import Table
from crewai.cli.command import BaseCommand
from crewai.cli.config import HIDDEN_SETTINGS_KEYS, READONLY_SETTINGS_KEYS, Settings
from crewai.events.listeners.tracing.utils import _load_user_data
from crewai_cli.command import BaseCommand
from crewai_cli.config import HIDDEN_SETTINGS_KEYS, READONLY_SETTINGS_KEYS, Settings
from crewai_cli.user_data import _load_user_data
console = Console()
@@ -91,7 +91,7 @@ class SettingsCommand(BaseCommand):
style="bold red",
)
console.print("Available keys:", style="yellow")
for field_name in Settings.model_fields.keys():
for field_name in Settings.model_fields:
if field_name not in readonly_settings:
console.print(f" - {field_name}", style="yellow")
raise SystemExit(1)

View File

@@ -0,0 +1,12 @@
"""Re-export of ``crewai_core.token_manager.TokenManager``.
Kept as a stable import path for the CLI; new code should import from
``crewai_core.token_manager`` directly.
"""
from __future__ import annotations
from crewai_core.token_manager import TokenManager as TokenManager
__all__ = ["TokenManager"]

View File

@@ -0,0 +1,67 @@
"""Lightweight SQLite reader for kickoff task outputs.
Only used by the ``crewai log-tasks-outputs`` CLI command. Depends solely on
the standard library + *appdirs* so crewai-cli can read stored outputs without
importing the full crewai framework.
"""
from __future__ import annotations
import json
import logging
from pathlib import Path
import sqlite3
from typing import Any
from crewai_cli.user_data import _db_storage_path
logger = logging.getLogger(__name__)
def load_task_outputs(db_path: str | None = None) -> list[dict[str, Any]]:
"""Return all rows from the kickoff task outputs database."""
if db_path is None:
db_path = str(Path(_db_storage_path()) / "latest_kickoff_task_outputs.db")
if not Path(db_path).exists():
return []
try:
with sqlite3.connect(db_path) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("""
SELECT task_id, expected_output, output, task_index,
inputs, was_replayed, timestamp
FROM latest_kickoff_task_outputs
ORDER BY task_index
""")
rows = cursor.fetchall()
except sqlite3.Error as e:
logger.error("Failed to load task outputs: %s", e)
return []
return [
{
"task_id": row["task_id"],
"expected_output": row["expected_output"],
"output": _safe_json_loads(row["output"]),
"task_index": row["task_index"],
"inputs": _safe_json_loads(row["inputs"]),
"was_replayed": row["was_replayed"],
"timestamp": row["timestamp"],
}
for row in rows
]
def _safe_json_loads(value: str | None) -> Any:
"""Decode a JSON column tolerantly: NULL/blank/corrupt → None."""
if not value:
return None
try:
return json.loads(value)
except (json.JSONDecodeError, TypeError) as e:
logger.warning("Failed to decode JSON column: %s", e)
return None

View File

@@ -10,14 +10,12 @@ from typing import Any
import click
from rich.console import Console
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
from crewai.cli.utils import (
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
from crewai_cli.utils import (
build_env_with_tool_repository_credentials,
extract_available_exports,
extract_tools_metadata,
get_project_description,
get_project_name,
get_project_version,
@@ -25,12 +23,37 @@ from crewai.cli.utils import (
tree_copy,
tree_find_and_replace,
)
from crewai.events.listeners.tracing.utils import get_user_id
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.
@@ -97,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(
@@ -106,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"
@@ -200,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,6 +1,6 @@
"""Triggers command module for CrewAI CLI."""
from crewai.cli.triggers.main import TriggersCommand
from crewai_cli.triggers.main import TriggersCommand
__all__ = ["TriggersCommand"]

View File

@@ -5,7 +5,7 @@ from typing import Any
from rich.console import Console
from rich.table import Table
from crewai.cli.command import BaseCommand, PlusAPIMixin
from crewai_cli.command import BaseCommand, PlusAPIMixin
console = Console()

View File

@@ -4,7 +4,7 @@ from typing import Any
import tomli_w
from crewai.cli.utils import read_toml
from crewai_cli.utils import read_toml
def update_crew() -> None:

View File

@@ -0,0 +1,22 @@
"""User-data helpers — re-exported from ``crewai_core.user_data``."""
from __future__ import annotations
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,
)
__all__ = [
"_db_storage_path",
"_load_user_data",
"_save_user_data",
"has_user_declined_tracing",
"is_tracing_enabled",
"update_user_data",
]

View File

@@ -0,0 +1,137 @@
from __future__ import annotations
import os
from pathlib import Path
import shutil
from typing import Any
import click
from crewai_core.project import (
get_project_description as get_project_description,
get_project_name as get_project_name,
get_project_version as get_project_version,
parse_toml as parse_toml,
read_toml as read_toml,
)
from crewai_core.tool_credentials import (
build_env_with_all_tool_credentials as build_env_with_all_tool_credentials,
build_env_with_tool_repository_credentials as build_env_with_tool_repository_credentials,
)
from rich.console import Console
__all__ = [
"build_env_with_all_tool_credentials",
"build_env_with_tool_repository_credentials",
"copy_template",
"fetch_and_json_env_file",
"get_project_description",
"get_project_name",
"get_project_version",
"load_env_vars",
"parse_toml",
"read_toml",
"tree_copy",
"tree_find_and_replace",
"write_env_file",
]
console = Console()
def copy_template(
src: Path, dst: Path, name: str, class_name: str, folder_name: str
) -> None:
"""Copy a file from src to dst."""
with open(src, "r") as file:
content = file.read()
content = content.replace("{{name}}", name)
content = content.replace("{{crew_name}}", class_name)
content = content.replace("{{folder_name}}", folder_name)
with open(dst, "w") as file:
file.write(content)
click.secho(f" - Created {dst}", fg="green")
def fetch_and_json_env_file(env_file_path: str = ".env") -> dict[str, Any]:
"""Fetch the environment variables from a .env file and return them as a dictionary."""
try:
with open(env_file_path, "r") as f:
env_content = f.read()
env_dict = {}
for line in env_content.splitlines():
if line.strip() and not line.strip().startswith("#"):
key, value = line.split("=", 1)
env_dict[key.strip()] = value.strip()
return env_dict
except FileNotFoundError:
console.print(f"Error: {env_file_path} not found.", style="bold red")
except Exception as e:
console.print(f"Error reading the .env file: {e}", style="bold red")
return {}
def tree_copy(source: Path, destination: Path) -> None:
"""Copies the entire directory structure from the source to the destination."""
for item in os.listdir(source):
source_item = os.path.join(source, item)
destination_item = os.path.join(destination, item)
if os.path.isdir(source_item):
shutil.copytree(source_item, destination_item)
else:
shutil.copy2(source_item, destination_item)
def tree_find_and_replace(directory: Path, find: str, replace: str) -> None:
"""Recursively searches through a directory, replacing a target string in
both file contents and filenames with a specified replacement string.
"""
for path, dirs, files in os.walk(os.path.abspath(directory), topdown=False):
for filename in files:
filepath = os.path.join(path, filename)
with open(filepath, "r", encoding="utf-8", errors="ignore") as file:
contents = file.read()
with open(filepath, "w") as file:
file.write(contents.replace(find, replace))
if find in filename:
new_filename = filename.replace(find, replace)
new_filepath = os.path.join(path, new_filename)
os.rename(filepath, new_filepath)
for dirname in dirs:
if find in dirname:
new_dirname = dirname.replace(find, replace)
new_dirpath = os.path.join(path, new_dirname)
old_dirpath = os.path.join(path, dirname)
os.rename(old_dirpath, new_dirpath)
def load_env_vars(folder_path: Path) -> dict[str, Any]:
"""Loads environment variables from a .env file in the specified folder path."""
env_file_path = folder_path / ".env"
env_vars = {}
if env_file_path.exists():
with open(env_file_path, "r") as file:
for line in file:
key, _, value = line.strip().partition("=")
if key and value:
env_vars[key] = value
return env_vars
def write_env_file(folder_path: Path, env_vars: dict[str, Any]) -> None:
"""Writes environment variables to a .env file in the specified folder."""
env_file_path = folder_path / ".env"
with open(env_file_path, "w") as file:
for key, value in env_vars.items():
file.write(f"{key.upper()}={value}\n")

View File

@@ -0,0 +1,24 @@
"""Re-exports of version utilities from ``crewai_core.version``.
Kept as a stable import path for the CLI; new code should import from
``crewai_core.version`` directly.
"""
from __future__ import annotations
from crewai_core.version import (
check_version as check_version,
get_crewai_version as get_crewai_version,
get_latest_version_from_pypi as get_latest_version_from_pypi,
is_current_version_yanked as is_current_version_yanked,
is_newer_version_available as is_newer_version_available,
)
__all__ = [
"check_version",
"get_crewai_version",
"get_latest_version_from_pypi",
"is_current_version_yanked",
"is_newer_version_available",
]

View File

@@ -0,0 +1,91 @@
import pytest
from crewai_cli.authentication.main import Oauth2Settings
from crewai_cli.authentication.providers.auth0 import Auth0Provider
class TestAuth0Provider:
@pytest.fixture(autouse=True)
def setup_method(self):
self.valid_settings = Oauth2Settings(
provider="auth0",
domain="test-domain.auth0.com",
client_id="test-client-id",
audience="test-audience"
)
self.provider = Auth0Provider(self.valid_settings)
def test_initialization_with_valid_settings(self):
provider = Auth0Provider(self.valid_settings)
assert provider.settings == self.valid_settings
assert provider.settings.provider == "auth0"
assert provider.settings.domain == "test-domain.auth0.com"
assert provider.settings.client_id == "test-client-id"
assert provider.settings.audience == "test-audience"
def test_get_authorize_url(self):
expected_url = "https://test-domain.auth0.com/oauth/device/code"
assert self.provider.get_authorize_url() == expected_url
def test_get_authorize_url_with_different_domain(self):
settings = Oauth2Settings(
provider="auth0",
domain="my-company.auth0.com",
client_id="test-client",
audience="test-audience"
)
provider = Auth0Provider(settings)
expected_url = "https://my-company.auth0.com/oauth/device/code"
assert provider.get_authorize_url() == expected_url
def test_get_token_url(self):
expected_url = "https://test-domain.auth0.com/oauth/token"
assert self.provider.get_token_url() == expected_url
def test_get_token_url_with_different_domain(self):
settings = Oauth2Settings(
provider="auth0",
domain="another-domain.auth0.com",
client_id="test-client",
audience="test-audience"
)
provider = Auth0Provider(settings)
expected_url = "https://another-domain.auth0.com/oauth/token"
assert provider.get_token_url() == expected_url
def test_get_jwks_url(self):
expected_url = "https://test-domain.auth0.com/.well-known/jwks.json"
assert self.provider.get_jwks_url() == expected_url
def test_get_jwks_url_with_different_domain(self):
settings = Oauth2Settings(
provider="auth0",
domain="dev.auth0.com",
client_id="test-client",
audience="test-audience"
)
provider = Auth0Provider(settings)
expected_url = "https://dev.auth0.com/.well-known/jwks.json"
assert provider.get_jwks_url() == expected_url
def test_get_issuer(self):
expected_issuer = "https://test-domain.auth0.com/"
assert self.provider.get_issuer() == expected_issuer
def test_get_issuer_with_different_domain(self):
settings = Oauth2Settings(
provider="auth0",
domain="prod.auth0.com",
client_id="test-client",
audience="test-audience"
)
provider = Auth0Provider(settings)
expected_issuer = "https://prod.auth0.com/"
assert provider.get_issuer() == expected_issuer
def test_get_audience(self):
assert self.provider.get_audience() == "test-audience"
def test_get_client_id(self):
assert self.provider.get_client_id() == "test-client-id"

View File

@@ -0,0 +1,141 @@
import pytest
from crewai_cli.authentication.main import Oauth2Settings
from crewai_cli.authentication.providers.entra_id import EntraIdProvider
class TestEntraIdProvider:
@pytest.fixture(autouse=True)
def setup_method(self):
self.valid_settings = Oauth2Settings(
provider="entra_id",
domain="tenant-id-abcdef123456",
client_id="test-client-id",
audience="test-audience",
extra={
"scope": "openid profile email api://crewai-cli-dev/read"
}
)
self.provider = EntraIdProvider(self.valid_settings)
def test_initialization_with_valid_settings(self):
provider = EntraIdProvider(self.valid_settings)
assert provider.settings == self.valid_settings
assert provider.settings.provider == "entra_id"
assert provider.settings.domain == "tenant-id-abcdef123456"
assert provider.settings.client_id == "test-client-id"
assert provider.settings.audience == "test-audience"
def test_get_authorize_url(self):
expected_url = "https://login.microsoftonline.com/tenant-id-abcdef123456/oauth2/v2.0/devicecode"
assert self.provider.get_authorize_url() == expected_url
def test_get_authorize_url_with_different_domain(self):
# For EntraID, the domain is the tenant ID.
settings = Oauth2Settings(
provider="entra_id",
domain="my-company.entra.id",
client_id="test-client",
audience="test-audience",
)
provider = EntraIdProvider(settings)
expected_url = "https://login.microsoftonline.com/my-company.entra.id/oauth2/v2.0/devicecode"
assert provider.get_authorize_url() == expected_url
def test_get_token_url(self):
expected_url = "https://login.microsoftonline.com/tenant-id-abcdef123456/oauth2/v2.0/token"
assert self.provider.get_token_url() == expected_url
def test_get_token_url_with_different_domain(self):
# For EntraID, the domain is the tenant ID.
settings = Oauth2Settings(
provider="entra_id",
domain="another-domain.entra.id",
client_id="test-client",
audience="test-audience",
)
provider = EntraIdProvider(settings)
expected_url = "https://login.microsoftonline.com/another-domain.entra.id/oauth2/v2.0/token"
assert provider.get_token_url() == expected_url
def test_get_jwks_url(self):
expected_url = "https://login.microsoftonline.com/tenant-id-abcdef123456/discovery/v2.0/keys"
assert self.provider.get_jwks_url() == expected_url
def test_get_jwks_url_with_different_domain(self):
# For EntraID, the domain is the tenant ID.
settings = Oauth2Settings(
provider="entra_id",
domain="dev.entra.id",
client_id="test-client",
audience="test-audience",
)
provider = EntraIdProvider(settings)
expected_url = "https://login.microsoftonline.com/dev.entra.id/discovery/v2.0/keys"
assert provider.get_jwks_url() == expected_url
def test_get_issuer(self):
expected_issuer = "https://login.microsoftonline.com/tenant-id-abcdef123456/v2.0"
assert self.provider.get_issuer() == expected_issuer
def test_get_issuer_with_different_domain(self):
# For EntraID, the domain is the tenant ID.
settings = Oauth2Settings(
provider="entra_id",
domain="other-tenant-id-xpto",
client_id="test-client",
audience="test-audience",
)
provider = EntraIdProvider(settings)
expected_issuer = "https://login.microsoftonline.com/other-tenant-id-xpto/v2.0"
assert provider.get_issuer() == expected_issuer
def test_get_audience(self):
assert self.provider.get_audience() == "test-audience"
def test_get_audience_assertion_error_when_none(self):
settings = Oauth2Settings(
provider="entra_id",
domain="test-tenant-id",
client_id="test-client-id",
audience=None,
)
provider = EntraIdProvider(settings)
with pytest.raises(ValueError, match="Audience is required"):
provider.get_audience()
def test_get_client_id(self):
assert self.provider.get_client_id() == "test-client-id"
def test_get_required_fields(self):
assert set(self.provider.get_required_fields()) == set(["scope"])
def test_get_oauth_scopes(self):
settings = Oauth2Settings(
provider="entra_id",
domain="tenant-id-abcdef123456",
client_id="test-client-id",
audience="test-audience",
extra={
"scope": "api://crewai-cli-dev/read"
}
)
provider = EntraIdProvider(settings)
assert provider.get_oauth_scopes() == ["openid", "profile", "email", "api://crewai-cli-dev/read"]
def test_get_oauth_scopes_with_multiple_custom_scopes(self):
settings = Oauth2Settings(
provider="entra_id",
domain="tenant-id-abcdef123456",
client_id="test-client-id",
audience="test-audience",
extra={
"scope": "api://crewai-cli-dev/read api://crewai-cli-dev/write custom-scope1 custom-scope2"
}
)
provider = EntraIdProvider(settings)
assert provider.get_oauth_scopes() == ["openid", "profile", "email", "api://crewai-cli-dev/read", "api://crewai-cli-dev/write", "custom-scope1", "custom-scope2"]
def test_base_url(self):
assert self.provider._base_url() == "https://login.microsoftonline.com/tenant-id-abcdef123456"

View File

@@ -0,0 +1,138 @@
import pytest
from crewai_cli.authentication.main import Oauth2Settings
from crewai_cli.authentication.providers.keycloak import KeycloakProvider
class TestKeycloakProvider:
@pytest.fixture(autouse=True)
def setup_method(self):
self.valid_settings = Oauth2Settings(
provider="keycloak",
domain="keycloak.example.com",
client_id="test-client-id",
audience="test-audience",
extra={
"realm": "test-realm"
}
)
self.provider = KeycloakProvider(self.valid_settings)
def test_initialization_with_valid_settings(self):
provider = KeycloakProvider(self.valid_settings)
assert provider.settings == self.valid_settings
assert provider.settings.provider == "keycloak"
assert provider.settings.domain == "keycloak.example.com"
assert provider.settings.client_id == "test-client-id"
assert provider.settings.audience == "test-audience"
assert provider.settings.extra.get("realm") == "test-realm"
def test_get_authorize_url(self):
expected_url = "https://keycloak.example.com/realms/test-realm/protocol/openid-connect/auth/device"
assert self.provider.get_authorize_url() == expected_url
def test_get_authorize_url_with_different_domain(self):
settings = Oauth2Settings(
provider="keycloak",
domain="auth.company.com",
client_id="test-client",
audience="test-audience",
extra={
"realm": "my-realm"
}
)
provider = KeycloakProvider(settings)
expected_url = "https://auth.company.com/realms/my-realm/protocol/openid-connect/auth/device"
assert provider.get_authorize_url() == expected_url
def test_get_token_url(self):
expected_url = "https://keycloak.example.com/realms/test-realm/protocol/openid-connect/token"
assert self.provider.get_token_url() == expected_url
def test_get_token_url_with_different_domain(self):
settings = Oauth2Settings(
provider="keycloak",
domain="sso.enterprise.com",
client_id="test-client",
audience="test-audience",
extra={
"realm": "enterprise-realm"
}
)
provider = KeycloakProvider(settings)
expected_url = "https://sso.enterprise.com/realms/enterprise-realm/protocol/openid-connect/token"
assert provider.get_token_url() == expected_url
def test_get_jwks_url(self):
expected_url = "https://keycloak.example.com/realms/test-realm/protocol/openid-connect/certs"
assert self.provider.get_jwks_url() == expected_url
def test_get_jwks_url_with_different_domain(self):
settings = Oauth2Settings(
provider="keycloak",
domain="identity.org",
client_id="test-client",
audience="test-audience",
extra={
"realm": "org-realm"
}
)
provider = KeycloakProvider(settings)
expected_url = "https://identity.org/realms/org-realm/protocol/openid-connect/certs"
assert provider.get_jwks_url() == expected_url
def test_get_issuer(self):
expected_issuer = "https://keycloak.example.com/realms/test-realm"
assert self.provider.get_issuer() == expected_issuer
def test_get_issuer_with_different_domain(self):
settings = Oauth2Settings(
provider="keycloak",
domain="login.myapp.io",
client_id="test-client",
audience="test-audience",
extra={
"realm": "app-realm"
}
)
provider = KeycloakProvider(settings)
expected_issuer = "https://login.myapp.io/realms/app-realm"
assert provider.get_issuer() == expected_issuer
def test_get_audience(self):
assert self.provider.get_audience() == "test-audience"
def test_get_client_id(self):
assert self.provider.get_client_id() == "test-client-id"
def test_get_required_fields(self):
assert self.provider.get_required_fields() == ["realm"]
def test_oauth2_base_url(self):
assert self.provider._oauth2_base_url() == "https://keycloak.example.com"
def test_oauth2_base_url_strips_https_prefix(self):
settings = Oauth2Settings(
provider="keycloak",
domain="https://keycloak.example.com",
client_id="test-client-id",
audience="test-audience",
extra={
"realm": "test-realm"
}
)
provider = KeycloakProvider(settings)
assert provider._oauth2_base_url() == "https://keycloak.example.com"
def test_oauth2_base_url_strips_http_prefix(self):
settings = Oauth2Settings(
provider="keycloak",
domain="http://keycloak.example.com",
client_id="test-client-id",
audience="test-audience",
extra={
"realm": "test-realm"
}
)
provider = KeycloakProvider(settings)
assert provider._oauth2_base_url() == "https://keycloak.example.com"

Some files were not shown because too many files have changed in this diff Show More