mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-08-11 17:02:07 +00:00
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
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
Nightly Canary Release / Check for new commits (push) Has been cancelled
Nightly Canary Release / Build nightly packages (push) Has been cancelled
Nightly Canary Release / Publish nightly to PyPI (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
* feat: add project_id to link OSS usage to an enterprise account Adds a stable per-project identifier so a project's OSS traces and runs can be attributed to an account after signup. There was no such identifier before: [tool.crewai] held only `type`, the deploy UUID was printed to the console but never persisted, Settings.org_uuid is global rather than per-project, and trace batches carried only crew_fingerprint/crew_name. The id lives in the project's pyproject.toml, so it is committed with the repository and stays stable across machines, teammates, CI, and containers - unlike a machine- or user-derived identifier, which is unstable in exactly the containerized production environments that matter most. crewai-core: - get_project_id(): read-only lookup of [tool.crewai].project_id. Safe for library code; never creates or modifies anything. - get_or_create_project_id(): mints a uuid4 and persists it, returning (id, created) so callers can tell the user. Best-effort - returns (None, False) for a missing, malformed, or read-only pyproject.toml rather than raising. - Insertion edits the raw TOML text instead of round-tripping through a writer, so comments, key order, and formatting elsewhere survive. The key is placed at the end of the [tool.crewai] table, before the next table header, so it cannot land in a neighbouring section. - LoginPayload and TraceExecutionContext gain optional project_id. Sent on two paths: - Traces: project_id is added to execution_context, which is sent on both the ephemeral and authenticated paths, so a project's traces remain attributable before and after the user creates an account. - Login: `crewai login` already sends the pseudonymous user_identifier on an authenticated request; adding project_id means one request carries account + user + project, which is the link itself. Minting is restricted to CLI commands the user explicitly invoked - `crewai create` for new projects and `crewai run` to backfill existing ones - and is announced when it happens. Library code only ever reads. Silently rewriting a user's pyproject.toml during Crew.kickoff() would be surprising. Privacy: project_id is a random uuid4 in a file the user commits. It is visible in a diff, contains nothing personal, and identifies a project rather than a person - so this needs none of the notice changes that attaching a user identifier to all telemetry would require. Tests: 18 new tests covering minting, stability, table placement, comment and formatting preservation, five pyproject layouts, the neighbouring-table regression, and graceful handling of missing/malformed/read-only files. Verified end-to-end that both create paths mint distinct ids, that the trace payload carries project_id on both the ephemeral and authenticated paths, and that the login payload carries user_identifier and project_id together. Follow-ups, deliberately not included: adding project_id to telemetry spans, and backend persistence of the (account, user_identifier, project_id) triple. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UNumDnNbiyw3pv1WakAe6t * refactor: drop the console announcement when minting project_id Minting now happens silently. With no message to print, the (id, created) tuple had no consumer, so simplify the API rather than keep the flag around for a hypothetical caller: - get_or_create_project_id() returns `str | None` instead of `tuple[str | None, bool]`. - Remove crewai_cli.utils.ensure_project_id, which existed only to print the message and discard the flag. The four call sites (crewai create crew, crewai create flow, crewai run, and tool-repository login) now call get_or_create_project_id directly. - Update tests for the simplified signature; still 18 tests covering minting, stability, table placement, formatting preservation, five pyproject layouts, and missing/malformed/read-only handling. Behaviour is otherwise unchanged: minting stays restricted to CLI commands the user invoked, library code still only reads via get_project_id, and a missing or read-only pyproject.toml still returns None rather than raising. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UNumDnNbiyw3pv1WakAe6t * fix: harden project_id minting against TOML corruption; address review Several reviewers found ways the raw-text edit could produce invalid TOML. Each is now fixed and covered by a test that fails without the fix. Duplicate project_id key (Cursor bugbot, Copilot x2): - get_project_id() reports a blank or non-string value as "absent", so a file containing `project_id = ""` took the insert path and gained a second project_id line - a duplicate key, and therefore invalid TOML that no tomli-based tool could read afterwards. - _insert_project_id is now _set_project_id: it replaces an existing assignment inside [tool.crewai] instead of appending unconditionally. Table header with a trailing comment (CodeRabbit major, Cursor bugbot): - `[tool.crewai] # config` is valid TOML but failed exact string equality, so the fallback appended a second [tool.crewai] header - a redefined table, also invalid TOML, and silent because get_project_id swallows the resulting decode error. - Added _is_table_header(), which tolerates a trailing comment and does not match similar names such as [tool.crewai-extra]. Writing into malformed TOML (Cursor bugbot, Copilot): - get_or_create_project_id relied on get_project_id, which cannot distinguish "no id" from "unparsable file", so it appended to files it could not parse. - The locked path now parses explicitly and bails on a decode error, and re-parses the updated content before writing, so this feature can never be the reason a project's pyproject.toml stops parsing. Concurrency and atomicity (CodeRabbit major): - Two CLI processes could both see no id, mint different uuids, and clobber each other, leaving a caller holding an id that is not on disk. Minting now takes the existing crewai_core cross-process lock, re-reads under it, and returns the id that persists. - Writes go through a temp file in the same directory plus os.replace, so an interruption cannot truncate pyproject.toml. File mode is copied across, and the temp file is removed on failure. - os.replace only needs a writable directory, which would have let an atomic write silently overwrite a file the user marked read-only; writability is now checked explicitly so that case still returns None. Line endings (CodeRabbit): - Path.read_text/write_text normalized CRLF to LF, so minting would rewrite a CRLF-committed file entirely. Read and write now use newline="" and the inserted line ending is derived from the existing content. Default create path skipped minting (Cursor bugbot): - `crewai create crew` defaults to create_json_crew; only the --classic and flow paths minted, so most new projects had no id until a later command. Wired into create_json_crew as well. Verified all three paths now mint distinct ids. Do not mint during login (CodeRabbit major): - ToolCommand.login ran get_or_create_project_id, which is outside the sanctioned minting commands and is invoked by `crewai tools create` from a freshly scaffolded directory before the project is persisted. It now uses the read-only get_project_id. Verified login leaves pyproject.toml untouched. Not applied: Copilot asked for a console message when an id is written, in create_crew and create_flow. Minting was made deliberately silent in the previous commit, so the (id, created) tuple and the announcement are both gone by design. Tests: 32 in test_project_id.py, up from 18. New cases cover blank and non-string existing ids, three commented-header forms, similar table names, malformed input, CRLF and LF preservation, concurrent minting convergence, file-mode preservation, and temp-file cleanup. Confirmed the header and duplicate-key tests fail when the fixes are reverted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UNumDnNbiyw3pv1WakAe6t * fix: never create [tool.crewai], treat whitespace ids as absent, harden test `crewai run` could rewrite unrelated projects (Cursor bugbot, high): - get_or_create_project_id ran before the cwd was established as a CrewAI project, and _set_project_id appended a [tool.crewai] table when none existed. Any directory with a pyproject.toml could therefore gain one - including on `crewai run --definition`, which may otherwise succeed. - _set_project_id no longer creates the table; it returns None when [tool.crewai] is absent, so a key is only ever added to a table the project already declares. The templates all ship the table, so no create path needs the old fallback. - The minting call in run_crew moved after the --definition early return, so an explicit-flow run does not touch the cwd at all. - Presence is checked, not truthiness: an empty [tool.crewai] is still a CrewAI marker, and get_crewai_project_config returns {} both for that and for an absent table. - Verified an unrelated project's pyproject.toml is byte-identical after a mint attempt. Whitespace-only project_id accepted as valid (CodeRabbit): - `project_id = " "` is truthy, so it was returned as an identity and would have propagated into login payloads and tracing context. It also meant the '" "' parameter of the replacement test asserted nothing. - Added _usable_project_id, which strips before deciding, used by both get_project_id and the locked mint path. Concurrency test could hang CI (CodeRabbit, major): - Neither the barrier nor the joins had timeouts, so a thread dying early or blocking on the lock would hang the job rather than fail it. The result count was also unchecked, so a dead thread still passed. - Added timeouts, an explicit liveness assertion, a result-count assertion, a lock around the shared result list, and corrected the docstring: this covers the read-modify-write race with threads, not the cross-process backend. Tests: 35, up from 32. New coverage for the absent-table refusal and three whitespace forms; the blank-id replacement case now asserts a real uuid replaced the blank value. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UNumDnNbiyw3pv1WakAe6t * chore(deps): force gitpython 3.1.57+ for GHSA-p538-c434-8v24 and GHSA-3f7w-8rr8-f37f Unrelated to project_id; bundled here only because it blocks this PR's vulnerability scan. Two advisories were published for gitpython 3.1.55 after main last passed the scan: - GHSA-p538-c434-8v24: arbitrary file truncation via `git rev-list --output` argument injection. Fixed in 3.1.56. - GHSA-3f7w-8rr8-f37f: unguarded git option forwarding in IndexFile.checkout() and TagReference. Fixed in 3.1.57. - Bump the override floor to gitpython>=3.1.57 and declare the same floor in crewai-tools, so consumers installing the published package are covered and not only this repo's lock. - 3.1.57 was published 2026-07-26, past gitpython's exclude-newer-package cutoff of 2026-07-24, so that cutoff moves to 2026-07-27. Without it the floor is unresolvable. pip-audit against the updated lock reports no known vulnerabilities. Verified gitpython 3.1.57 resolves and that crewai_tools and crewai_cli.git still import. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UNumDnNbiyw3pv1WakAe6t --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
373 lines
14 KiB
Python
373 lines
14 KiB
Python
import base64
|
|
from json import JSONDecodeError
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
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 (
|
|
build_env_with_tool_repository_credentials,
|
|
get_project_description,
|
|
get_project_id,
|
|
get_project_name,
|
|
get_project_version,
|
|
read_toml,
|
|
tree_copy,
|
|
tree_find_and_replace,
|
|
)
|
|
from crewai_cli.version import get_crewai_tools_dependency
|
|
|
|
|
|
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.
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
BaseCommand.__init__(self)
|
|
PlusAPIMixin.__init__(self, telemetry=self._telemetry)
|
|
|
|
def create(self, handle: str) -> None:
|
|
self._ensure_not_in_project()
|
|
|
|
folder_name = handle.replace(" ", "_").replace("-", "_").lower()
|
|
class_name = handle.replace("_", " ").replace("-", " ").title().replace(" ", "")
|
|
|
|
project_root = Path(folder_name)
|
|
if project_root.exists():
|
|
click.secho(f"Folder {folder_name} already exists.", fg="red")
|
|
raise SystemExit
|
|
os.makedirs(project_root)
|
|
|
|
click.secho(f"Creating custom tool {folder_name}...", fg="green", bold=True)
|
|
|
|
template_dir = Path(__file__).parent.parent / "templates" / "tool"
|
|
tree_copy(template_dir, project_root)
|
|
tree_find_and_replace(project_root, "{{folder_name}}", folder_name)
|
|
tree_find_and_replace(project_root, "{{class_name}}", class_name)
|
|
tree_find_and_replace(
|
|
project_root, "{{crewai_tools_dependency}}", get_crewai_tools_dependency()
|
|
)
|
|
|
|
agents_md_src = Path(__file__).parent.parent / "templates" / "AGENTS.md"
|
|
if agents_md_src.exists():
|
|
shutil.copy2(agents_md_src, project_root / "AGENTS.md")
|
|
|
|
old_directory = os.getcwd()
|
|
os.chdir(project_root)
|
|
try:
|
|
self.login()
|
|
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]"
|
|
)
|
|
finally:
|
|
os.chdir(old_directory)
|
|
|
|
def publish(self, is_public: bool, force: bool = False) -> None:
|
|
if not git.Repository().is_synced() and not force:
|
|
console.print(
|
|
"[bold red]Failed to publish tool.[/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 tool."
|
|
)
|
|
raise SystemExit()
|
|
|
|
project_name = get_project_name(require=True)
|
|
assert isinstance(project_name, str) # noqa: S101
|
|
|
|
project_version = get_project_version(require=True)
|
|
assert isinstance(project_version, str) # noqa: S101
|
|
|
|
project_description = get_project_description(require=False)
|
|
encoded_tarball = None
|
|
|
|
console.print("[bold blue]Discovering tools from your project...[/bold blue]")
|
|
project_utils = _require_project_utils()
|
|
available_exports = project_utils.extract_available_exports()
|
|
|
|
if available_exports:
|
|
console.print(
|
|
f"[green]Found these tools to publish: {', '.join([e['name'] for e in available_exports])}[/green]"
|
|
)
|
|
|
|
console.print("[bold blue]Extracting tool metadata...[/bold blue]")
|
|
try:
|
|
tools_metadata = project_utils.extract_tools_metadata()
|
|
except Exception as e:
|
|
console.print(
|
|
f"[yellow]Warning: Could not extract tool metadata: {e}[/yellow]\n"
|
|
f"Publishing will continue without detailed metadata."
|
|
)
|
|
tools_metadata = []
|
|
|
|
self._print_tools_preview(tools_metadata)
|
|
self._print_current_organization()
|
|
|
|
build_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)
|
|
build_env.update(index_env)
|
|
except Exception: # noqa: S110
|
|
pass
|
|
|
|
with tempfile.TemporaryDirectory() as temp_build_dir:
|
|
subprocess.run( # noqa: S603
|
|
["uv", "build", "--sdist", "--out-dir", temp_build_dir], # noqa: S607
|
|
check=True,
|
|
capture_output=False,
|
|
env=build_env,
|
|
)
|
|
|
|
tarball_filename = next(
|
|
(f for f in os.listdir(temp_build_dir) if f.endswith(".tar.gz")), None
|
|
)
|
|
if not tarball_filename:
|
|
console.print(
|
|
"Project build failed. Please ensure that the command `uv build --sdist` completes successfully.",
|
|
style="bold red",
|
|
)
|
|
raise SystemExit(1)
|
|
|
|
tarball_path = os.path.join(temp_build_dir, tarball_filename)
|
|
with open(tarball_path, "rb") as file:
|
|
tarball_contents = file.read()
|
|
|
|
encoded_tarball = base64.b64encode(tarball_contents).decode("utf-8")
|
|
|
|
console.print("[bold blue]Publishing tool to repository...[/bold blue]")
|
|
publish_response = self.plus_api_client.publish_tool(
|
|
handle=project_name,
|
|
is_public=is_public,
|
|
version=project_version,
|
|
description=project_description,
|
|
encoded_file=f"data:application/x-gzip;base64,{encoded_tarball}",
|
|
available_exports=available_exports,
|
|
tools_metadata=tools_metadata,
|
|
)
|
|
|
|
self._validate_response(publish_response)
|
|
|
|
published_handle = publish_response.json()["handle"]
|
|
settings = Settings()
|
|
base_url = settings.enterprise_base_url or DEFAULT_CREWAI_ENTERPRISE_URL
|
|
|
|
console.print(
|
|
f"Successfully published `{published_handle}` ({project_version}).\n\n"
|
|
+ "⚠️ Security checks are running in the background. Your tool will be available once these are complete.\n"
|
|
+ f"You can monitor the status or access your tool here:\n{base_url}/crewai_plus/tools/{published_handle}",
|
|
style="bold green",
|
|
)
|
|
|
|
def install(self, handle: str) -> None:
|
|
self._print_current_organization()
|
|
get_response = self.plus_api_client.get_tool(handle)
|
|
|
|
if get_response.status_code == 404:
|
|
console.print(
|
|
"No tool found with this name. Please ensure the tool was published and you have access to it.",
|
|
style="bold red",
|
|
)
|
|
raise SystemExit
|
|
if get_response.status_code != 200:
|
|
console.print(
|
|
"Failed to get tool details. Please try again later.", style="bold red"
|
|
)
|
|
raise SystemExit
|
|
|
|
self._add_package(get_response.json())
|
|
|
|
console.print(f"Successfully installed {handle}", style="bold green")
|
|
|
|
def login(self) -> None:
|
|
get_user_id = _require_get_user_id()
|
|
# Read-only: login is not one of the sanctioned minting commands, and
|
|
# `crewai tools create` calls it from inside a freshly scaffolded
|
|
# directory before the tool project is persisted.
|
|
login_response = self.plus_api_client.login_to_tool_repository(
|
|
user_identifier=get_user_id(),
|
|
project_id=get_project_id(),
|
|
)
|
|
|
|
if login_response.status_code != 200:
|
|
console.print(
|
|
"Authentication failed. Verify if the currently active organization can access the tool repository, and run 'crewai login' again.",
|
|
style="bold red",
|
|
)
|
|
try:
|
|
console.print(
|
|
f"[{login_response.status_code} error - {login_response.json().get('message', 'Unknown error')}]",
|
|
style="bold red italic",
|
|
)
|
|
except JSONDecodeError:
|
|
console.print(
|
|
f"[{login_response.status_code} error - Unknown error - Invalid JSON response]",
|
|
style="bold red italic",
|
|
)
|
|
raise SystemExit
|
|
|
|
login_response_json = login_response.json()
|
|
|
|
settings = Settings()
|
|
settings.tool_repository_username = login_response_json["credential"][
|
|
"username"
|
|
]
|
|
settings.tool_repository_password = login_response_json["credential"][
|
|
"password"
|
|
]
|
|
settings.org_uuid = login_response_json["current_organization"]["uuid"]
|
|
settings.org_name = login_response_json["current_organization"]["name"]
|
|
settings.dump()
|
|
|
|
def _add_package(self, tool_details: dict[str, Any]) -> None:
|
|
is_from_pypi = tool_details.get("source", None) == "pypi"
|
|
tool_handle = tool_details["handle"]
|
|
repository_handle = tool_details["repository"]["handle"]
|
|
repository_url = tool_details["repository"]["url"]
|
|
index = f"{repository_handle}={repository_url}"
|
|
|
|
add_package_command = [
|
|
"uv",
|
|
"add",
|
|
]
|
|
|
|
if is_from_pypi:
|
|
add_package_command.append(tool_handle)
|
|
else:
|
|
add_package_command.extend(["--index", index, tool_handle])
|
|
|
|
add_package_result = subprocess.run( # noqa: S603
|
|
add_package_command,
|
|
capture_output=False,
|
|
env=build_env_with_tool_repository_credentials(repository_handle),
|
|
text=True,
|
|
check=True,
|
|
)
|
|
|
|
if add_package_result.stderr:
|
|
click.echo(add_package_result.stderr, err=True)
|
|
raise SystemExit
|
|
|
|
def _ensure_not_in_project(self) -> None:
|
|
if os.path.isfile("./pyproject.toml"):
|
|
console.print(
|
|
"[bold red]Oops! It looks like you're inside a project.[/bold red]"
|
|
)
|
|
console.print(
|
|
"You can't create a new tool while inside an existing project."
|
|
)
|
|
console.print(
|
|
"[bold yellow]Tip:[/bold yellow] Navigate to a different directory and try again."
|
|
)
|
|
raise SystemExit
|
|
|
|
def _print_tools_preview(self, tools_metadata: list[dict[str, Any]]) -> None:
|
|
if not tools_metadata:
|
|
console.print("[yellow]No tool metadata extracted.[/yellow]")
|
|
return
|
|
|
|
console.print(
|
|
f"\n[bold]Tools to be published ({len(tools_metadata)}):[/bold]\n"
|
|
)
|
|
|
|
for tool in tools_metadata:
|
|
console.print(f" [bold cyan]{tool.get('name', 'Unknown')}[/bold cyan]")
|
|
if tool.get("module"):
|
|
console.print(f" Module: {tool.get('module')}")
|
|
console.print(f" Name: {tool.get('humanized_name', 'N/A')}")
|
|
console.print(
|
|
f" Description: {tool.get('description', 'N/A')[:80]}{'...' if len(tool.get('description', '')) > 80 else ''}"
|
|
)
|
|
|
|
init_params = tool.get("init_params_schema", {}).get("properties", {})
|
|
if init_params:
|
|
required = tool.get("init_params_schema", {}).get("required", [])
|
|
console.print(" Init parameters:")
|
|
for param_name, param_info in init_params.items():
|
|
param_type = param_info.get("type", "any")
|
|
is_required = param_name in required
|
|
req_marker = "[red]*[/red]" if is_required else ""
|
|
default = (
|
|
f" = {param_info['default']}" if "default" in param_info else ""
|
|
)
|
|
console.print(
|
|
f" - {param_name}: {param_type}{default} {req_marker}"
|
|
)
|
|
|
|
env_vars = tool.get("env_vars", [])
|
|
if env_vars:
|
|
console.print(" Environment variables:")
|
|
for env_var in env_vars:
|
|
req_marker = "[red]*[/red]" if env_var.get("required") else ""
|
|
default = (
|
|
f" (default: {env_var['default']})"
|
|
if env_var.get("default")
|
|
else ""
|
|
)
|
|
console.print(
|
|
f" - {env_var['name']}: {env_var.get('description', 'N/A')}{default} {req_marker}"
|
|
)
|
|
|
|
console.print()
|
|
|
|
def _print_current_organization(self) -> None:
|
|
settings = Settings()
|
|
if settings.org_uuid:
|
|
console.print(
|
|
f"Current organization: {settings.org_name} ({settings.org_uuid})",
|
|
style="bold blue",
|
|
)
|
|
else:
|
|
console.print(
|
|
"No organization currently set. We recommend setting one before using: `crewai org switch <org_id>` command.",
|
|
style="yellow",
|
|
)
|