dhub-cli 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. dhub_cli-0.1.0/.gitignore +22 -0
  2. dhub_cli-0.1.0/PKG-INFO +78 -0
  3. dhub_cli-0.1.0/README.md +52 -0
  4. dhub_cli-0.1.0/pyproject.toml +50 -0
  5. dhub_cli-0.1.0/src/dhub/__init__.py +1 -0
  6. dhub_cli-0.1.0/src/dhub/cli/__init__.py +1 -0
  7. dhub_cli-0.1.0/src/dhub/cli/app.py +35 -0
  8. dhub_cli-0.1.0/src/dhub/cli/auth.py +93 -0
  9. dhub_cli-0.1.0/src/dhub/cli/config.py +74 -0
  10. dhub_cli-0.1.0/src/dhub/cli/keys.py +96 -0
  11. dhub_cli-0.1.0/src/dhub/cli/org.py +125 -0
  12. dhub_cli-0.1.0/src/dhub/cli/registry.py +254 -0
  13. dhub_cli-0.1.0/src/dhub/cli/runtime.py +87 -0
  14. dhub_cli-0.1.0/src/dhub/cli/search.py +36 -0
  15. dhub_cli-0.1.0/src/dhub/core/__init__.py +1 -0
  16. dhub_cli-0.1.0/src/dhub/core/install.py +159 -0
  17. dhub_cli-0.1.0/src/dhub/core/manifest.py +221 -0
  18. dhub_cli-0.1.0/src/dhub/core/runtime.py +84 -0
  19. dhub_cli-0.1.0/src/dhub/core/validation.py +50 -0
  20. dhub_cli-0.1.0/src/dhub/models.py +38 -0
  21. dhub_cli-0.1.0/tests/__init__.py +1 -0
  22. dhub_cli-0.1.0/tests/conftest.py +1 -0
  23. dhub_cli-0.1.0/tests/test_cli/__init__.py +1 -0
  24. dhub_cli-0.1.0/tests/test_cli/test_auth_cli.py +98 -0
  25. dhub_cli-0.1.0/tests/test_cli/test_keys_cli.py +109 -0
  26. dhub_cli-0.1.0/tests/test_cli/test_org_cli.py +215 -0
  27. dhub_cli-0.1.0/tests/test_cli/test_registry_cli.py +468 -0
  28. dhub_cli-0.1.0/tests/test_cli/test_runtime_cli.py +181 -0
  29. dhub_cli-0.1.0/tests/test_cli/test_search_cli.py +90 -0
  30. dhub_cli-0.1.0/tests/test_core/__init__.py +1 -0
  31. dhub_cli-0.1.0/tests/test_core/test_docx_integration.py +271 -0
  32. dhub_cli-0.1.0/tests/test_core/test_install.py +52 -0
  33. dhub_cli-0.1.0/tests/test_core/test_install_symlinks.py +168 -0
  34. dhub_cli-0.1.0/tests/test_core/test_manifest.py +353 -0
  35. dhub_cli-0.1.0/tests/test_core/test_runtime.py +174 -0
  36. dhub_cli-0.1.0/tests/test_core/test_validation.py +74 -0
@@ -0,0 +1,22 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .eggs/
8
+ *.egg
9
+
10
+ .env
11
+ .venv/
12
+ venv/
13
+
14
+ .pytest_cache/
15
+ .mypy_cache/
16
+ .coverage
17
+ htmlcov/
18
+
19
+ *.db
20
+ *.sqlite3
21
+
22
+ .DS_Store
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: dhub-cli
3
+ Version: 0.1.0
4
+ Summary: The CLI package manager for AI agent skills
5
+ Project-URL: Homepage, https://github.com/lfiaschi/decision-hub
6
+ Project-URL: Repository, https://github.com/lfiaschi/decision-hub
7
+ Project-URL: Issues, https://github.com/lfiaschi/decision-hub/issues
8
+ Author-email: Luca Fiaschi <luca.fiaschi@gmail.com>
9
+ License-Expression: MIT
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Software Development :: Libraries
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: httpx>=0.27.0
20
+ Requires-Dist: pyyaml>=6.0.0
21
+ Requires-Dist: rich>=13.0.0
22
+ Requires-Dist: typer[all]>=0.12.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # dhub
28
+
29
+ The CLI package manager for AI agent skills.
30
+
31
+ `dhub` lets you publish, discover, and install **Skills** — modular capabilities (code + prompts) that agents like Claude, Cursor, and Gemini can use.
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ # Via uv
37
+ uv tool install dhub
38
+
39
+ # Via pipx
40
+ pipx install dhub
41
+ ```
42
+
43
+ ## Quick Start
44
+
45
+ ```bash
46
+ # Authenticate via GitHub
47
+ dhub login
48
+
49
+ # Publish a skill
50
+ dhub publish --org my-org --name my-skill --version 1.0.0
51
+
52
+ # Install a skill
53
+ dhub install my-org/my-skill
54
+
55
+ # Search for skills
56
+ dhub ask "analyze A/B test results"
57
+
58
+ # Run a skill locally
59
+ dhub run my-org/my-skill
60
+ ```
61
+
62
+ ## Commands
63
+
64
+ | Command | Description |
65
+ |---------|-------------|
66
+ | `dhub login` | Authenticate via GitHub Device Flow |
67
+ | `dhub publish` | Publish a skill to the registry |
68
+ | `dhub install` | Install a skill |
69
+ | `dhub list` | List installed skills |
70
+ | `dhub delete` | Delete a skill from the registry |
71
+ | `dhub run` | Run a locally installed skill |
72
+ | `dhub ask` | Natural language skill search |
73
+ | `dhub org` | Manage organizations |
74
+ | `dhub keys` | Manage API keys for evaluations |
75
+
76
+ ## Documentation
77
+
78
+ See the [main repository](https://github.com/lfiaschi/decision-hub) for full documentation.
@@ -0,0 +1,52 @@
1
+ # dhub
2
+
3
+ The CLI package manager for AI agent skills.
4
+
5
+ `dhub` lets you publish, discover, and install **Skills** — modular capabilities (code + prompts) that agents like Claude, Cursor, and Gemini can use.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ # Via uv
11
+ uv tool install dhub
12
+
13
+ # Via pipx
14
+ pipx install dhub
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ```bash
20
+ # Authenticate via GitHub
21
+ dhub login
22
+
23
+ # Publish a skill
24
+ dhub publish --org my-org --name my-skill --version 1.0.0
25
+
26
+ # Install a skill
27
+ dhub install my-org/my-skill
28
+
29
+ # Search for skills
30
+ dhub ask "analyze A/B test results"
31
+
32
+ # Run a skill locally
33
+ dhub run my-org/my-skill
34
+ ```
35
+
36
+ ## Commands
37
+
38
+ | Command | Description |
39
+ |---------|-------------|
40
+ | `dhub login` | Authenticate via GitHub Device Flow |
41
+ | `dhub publish` | Publish a skill to the registry |
42
+ | `dhub install` | Install a skill |
43
+ | `dhub list` | List installed skills |
44
+ | `dhub delete` | Delete a skill from the registry |
45
+ | `dhub run` | Run a locally installed skill |
46
+ | `dhub ask` | Natural language skill search |
47
+ | `dhub org` | Manage organizations |
48
+ | `dhub keys` | Manage API keys for evaluations |
49
+
50
+ ## Documentation
51
+
52
+ See the [main repository](https://github.com/lfiaschi/decision-hub) for full documentation.
@@ -0,0 +1,50 @@
1
+ [project]
2
+ name = "dhub-cli"
3
+ version = "0.1.0"
4
+ description = "The CLI package manager for AI agent skills"
5
+ requires-python = ">=3.11"
6
+ license = "MIT"
7
+ authors = [
8
+ { name = "Luca Fiaschi", email = "luca.fiaschi@gmail.com" },
9
+ ]
10
+ readme = "README.md"
11
+ classifiers = [
12
+ "Development Status :: 3 - Alpha",
13
+ "Environment :: Console",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Topic :: Software Development :: Libraries",
20
+ ]
21
+ dependencies = [
22
+ "typer[all]>=0.12.0",
23
+ "rich>=13.0.0",
24
+ "httpx>=0.27.0",
25
+ "pyyaml>=6.0.0",
26
+ ]
27
+
28
+ [project.optional-dependencies]
29
+ dev = [
30
+ "pytest>=8.0.0",
31
+ ]
32
+
33
+ [project.scripts]
34
+ dhub = "dhub.cli.app:run"
35
+
36
+ [build-system]
37
+ requires = ["hatchling"]
38
+ build-backend = "hatchling.build"
39
+
40
+ [tool.hatch.build.targets.wheel]
41
+ packages = ["src/dhub"]
42
+
43
+ [project.urls]
44
+ Homepage = "https://github.com/lfiaschi/decision-hub"
45
+ Repository = "https://github.com/lfiaschi/decision-hub"
46
+ Issues = "https://github.com/lfiaschi/decision-hub/issues"
47
+
48
+ [tool.pytest.ini_options]
49
+ testpaths = ["tests"]
50
+ pythonpath = ["src"]
@@ -0,0 +1 @@
1
+
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,35 @@
1
+ """Main Typer app with subcommand registration."""
2
+
3
+ import typer
4
+
5
+ app = typer.Typer(
6
+ name="dhub",
7
+ help="Decision Hub - The package manager for AI agent skills",
8
+ no_args_is_help=True,
9
+ )
10
+
11
+ # Register top-level commands
12
+ from dhub.cli.auth import login_command # noqa: E402
13
+ from dhub.cli.registry import delete_command, install_command, list_command, publish_command # noqa: E402
14
+ from dhub.cli.runtime import run_command # noqa: E402
15
+ from dhub.cli.search import ask_command # noqa: E402
16
+
17
+ app.command("login")(login_command)
18
+ app.command("publish")(publish_command)
19
+ app.command("install")(install_command)
20
+ app.command("list")(list_command)
21
+ app.command("delete")(delete_command)
22
+ app.command("run")(run_command)
23
+ app.command("ask")(ask_command)
24
+
25
+ # Register subcommand groups
26
+ from dhub.cli.keys import keys_app # noqa: E402
27
+ from dhub.cli.org import org_app # noqa: E402
28
+
29
+ app.add_typer(org_app, name="org")
30
+ app.add_typer(keys_app, name="keys")
31
+
32
+
33
+ def run() -> None:
34
+ """Entry point for the dhub CLI."""
35
+ app()
@@ -0,0 +1,93 @@
1
+ """Login via GitHub Device Flow."""
2
+
3
+ import time
4
+
5
+ import httpx
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+
10
+ console = Console()
11
+
12
+
13
+ def login_command(
14
+ api_url: str = typer.Option(None, "--api-url", help="API URL override"),
15
+ ) -> None:
16
+ """Authenticate with Decision Hub via GitHub."""
17
+ from dhub.cli.config import CliConfig, get_api_url, save_config
18
+
19
+ base_url = api_url or get_api_url()
20
+
21
+ # Step 1: Request a device code from the API
22
+ with httpx.Client() as client:
23
+ resp = client.post(f"{base_url}/auth/github/code")
24
+ resp.raise_for_status()
25
+ data = resp.json()
26
+
27
+ device_code: str = data["device_code"]
28
+ user_code: str = data["user_code"]
29
+ verification_uri: str = data["verification_uri"]
30
+ poll_interval: int = data.get("interval", 5)
31
+
32
+ # Step 2: Show the user code and URL
33
+ console.print(
34
+ Panel(
35
+ f"Open [bold blue]{verification_uri}[/] and enter code: "
36
+ f"[bold green]{user_code}[/]",
37
+ title="GitHub Login",
38
+ )
39
+ )
40
+ console.print("Waiting for authorization...")
41
+
42
+ # Step 3: Poll for the token until the user completes the flow
43
+ token_data = _poll_for_token(base_url, device_code, poll_interval)
44
+
45
+ # Step 4: Persist the token
46
+ new_config = CliConfig(api_url=base_url, token=token_data["access_token"])
47
+ save_config(new_config)
48
+
49
+ console.print(f"[green]Authenticated as @{token_data['username']}[/]")
50
+
51
+
52
+ def _poll_for_token(
53
+ base_url: str,
54
+ device_code: str,
55
+ interval: int,
56
+ timeout_seconds: int = 300,
57
+ ) -> dict:
58
+ """Poll the token endpoint until authorization succeeds or times out.
59
+
60
+ Args:
61
+ base_url: API base URL.
62
+ device_code: The device code returned from the code request.
63
+ interval: Seconds to wait between poll attempts.
64
+ timeout_seconds: Maximum total seconds to wait before giving up.
65
+
66
+ Returns:
67
+ Parsed JSON response containing 'access_token' and 'username'.
68
+
69
+ Raises:
70
+ typer.Exit: If the flow times out or the server rejects the request.
71
+ """
72
+ deadline = time.monotonic() + timeout_seconds
73
+
74
+ with httpx.Client(timeout=30) as client:
75
+ while time.monotonic() < deadline:
76
+ resp = client.post(
77
+ f"{base_url}/auth/github/token",
78
+ json={"device_code": device_code},
79
+ )
80
+
81
+ if resp.status_code == 200:
82
+ return resp.json()
83
+
84
+ # 428 means "authorization_pending" -- keep polling
85
+ if resp.status_code == 428:
86
+ time.sleep(interval)
87
+ continue
88
+
89
+ # Any other error is fatal
90
+ resp.raise_for_status()
91
+
92
+ console.print("[red]Error: Login timed out. Please try again.[/]")
93
+ raise typer.Exit(1)
@@ -0,0 +1,74 @@
1
+ """CLI configuration file management for ~/.dhub/config.json."""
2
+
3
+ import json
4
+ import os
5
+ from dataclasses import asdict, dataclass
6
+ from pathlib import Path
7
+
8
+ import typer
9
+
10
+ CONFIG_DIR = Path.home() / ".dhub"
11
+ CONFIG_FILE = CONFIG_DIR / "config.json"
12
+ DEFAULT_API_URL = "https://decision-hub--api.modal.run"
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class CliConfig:
17
+ """Immutable CLI configuration."""
18
+
19
+ api_url: str = DEFAULT_API_URL
20
+ token: str | None = None
21
+
22
+
23
+ def load_config() -> CliConfig:
24
+ """Load CLI config from ~/.dhub/config.json.
25
+
26
+ Returns defaults if the file does not exist or contains
27
+ incomplete data.
28
+ """
29
+ if not CONFIG_FILE.exists():
30
+ return CliConfig()
31
+
32
+ raw = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
33
+ return CliConfig(
34
+ api_url=raw.get("api_url", DEFAULT_API_URL),
35
+ token=raw.get("token"),
36
+ )
37
+
38
+
39
+ def save_config(config: CliConfig) -> None:
40
+ """Save CLI config to ~/.dhub/config.json.
41
+
42
+ Creates the ~/.dhub directory if it does not already exist.
43
+ """
44
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
45
+ CONFIG_FILE.write_text(
46
+ json.dumps(asdict(config), indent=2) + "\n",
47
+ encoding="utf-8",
48
+ )
49
+
50
+
51
+ def get_api_url() -> str:
52
+ """Get API URL from the DHUB_API_URL env var, falling back to saved config."""
53
+ env_url = os.environ.get("DHUB_API_URL")
54
+ if env_url:
55
+ return env_url
56
+ return load_config().api_url
57
+
58
+
59
+ def get_token() -> str:
60
+ """Get the stored auth token.
61
+
62
+ Raises:
63
+ typer.Exit: If no token is stored (user not logged in).
64
+ """
65
+ from rich.console import Console
66
+
67
+ token = load_config().token
68
+ if not token:
69
+ console = Console(stderr=True)
70
+ console.print(
71
+ "[red]Error: Not logged in. Run [bold]dhub login[/bold] first.[/]"
72
+ )
73
+ raise typer.Exit(1)
74
+ return token
@@ -0,0 +1,96 @@
1
+ """API key management commands."""
2
+
3
+ import httpx
4
+ import typer
5
+ from rich.console import Console
6
+ from rich.table import Table
7
+
8
+ console = Console()
9
+ keys_app = typer.Typer(help="Manage API keys for agent evals", no_args_is_help=True)
10
+
11
+
12
+ def _headers() -> dict[str, str]:
13
+ """Build authorization headers using the stored token."""
14
+ from dhub.cli.config import get_token
15
+
16
+ return {"Authorization": f"Bearer {get_token()}"}
17
+
18
+
19
+ def _api_url() -> str:
20
+ """Retrieve the configured API URL."""
21
+ from dhub.cli.config import get_api_url
22
+
23
+ return get_api_url()
24
+
25
+
26
+ @keys_app.command("add")
27
+ def add_key(
28
+ key_name: str = typer.Argument(help="Name for the API key"),
29
+ ) -> None:
30
+ """Add an API key (prompts for the value securely)."""
31
+ key_value = typer.prompt("Enter API key value", hide_input=True)
32
+
33
+ if not key_value.strip():
34
+ console.print("[red]Error: Key value cannot be empty.[/]")
35
+ raise typer.Exit(1)
36
+
37
+ with httpx.Client() as client:
38
+ resp = client.post(
39
+ f"{_api_url()}/v1/keys",
40
+ headers=_headers(),
41
+ json={"key_name": key_name, "value": key_value},
42
+ )
43
+ if resp.status_code == 409:
44
+ console.print(
45
+ f"[red]Error: Key '{key_name}' already exists. "
46
+ "Remove it first with [bold]dhub keys remove[/bold].[/]"
47
+ )
48
+ raise typer.Exit(1)
49
+ resp.raise_for_status()
50
+
51
+ console.print(f"[green]Added key: {key_name}[/]")
52
+
53
+
54
+ @keys_app.command("list")
55
+ def list_keys() -> None:
56
+ """List stored API key names."""
57
+ with httpx.Client() as client:
58
+ resp = client.get(
59
+ f"{_api_url()}/v1/keys",
60
+ headers=_headers(),
61
+ )
62
+ resp.raise_for_status()
63
+ keys = resp.json()
64
+
65
+ if not keys:
66
+ console.print("No API keys stored.")
67
+ return
68
+
69
+ table = Table(title="API Keys")
70
+ table.add_column("Name", style="cyan")
71
+ table.add_column("Created", style="dim")
72
+
73
+ for key in keys:
74
+ table.add_row(key.get("key_name", ""), key.get("created_at", ""))
75
+
76
+ console.print(table)
77
+
78
+
79
+ @keys_app.command("remove")
80
+ def remove_key(
81
+ key_name: str = typer.Argument(help="Name of the API key to remove"),
82
+ ) -> None:
83
+ """Remove a stored API key."""
84
+ with httpx.Client() as client:
85
+ resp = client.delete(
86
+ f"{_api_url()}/v1/keys/{key_name}",
87
+ headers=_headers(),
88
+ )
89
+ if resp.status_code == 404:
90
+ console.print(
91
+ f"[red]Error: Key '{key_name}' not found.[/]"
92
+ )
93
+ raise typer.Exit(1)
94
+ resp.raise_for_status()
95
+
96
+ console.print(f"[green]Removed key: {key_name}[/]")
@@ -0,0 +1,125 @@
1
+ """Organization management commands."""
2
+
3
+ import httpx
4
+ import typer
5
+ from rich.console import Console
6
+ from rich.table import Table
7
+
8
+ console = Console()
9
+ org_app = typer.Typer(help="Manage organizations", no_args_is_help=True)
10
+
11
+
12
+ def _headers() -> dict[str, str]:
13
+ """Build authorization headers using the stored token."""
14
+ from dhub.cli.config import get_token
15
+
16
+ return {"Authorization": f"Bearer {get_token()}"}
17
+
18
+
19
+ def _api_url() -> str:
20
+ """Retrieve the configured API URL."""
21
+ from dhub.cli.config import get_api_url
22
+
23
+ return get_api_url()
24
+
25
+
26
+ @org_app.command("create")
27
+ def create_org(slug: str = typer.Argument(help="Organization slug")) -> None:
28
+ """Create a new organization."""
29
+ with httpx.Client() as client:
30
+ resp = client.post(
31
+ f"{_api_url()}/v1/orgs",
32
+ headers=_headers(),
33
+ json={"slug": slug},
34
+ )
35
+ if resp.status_code == 409:
36
+ console.print(
37
+ f"[red]Error: Organization '{slug}' already exists.[/]"
38
+ )
39
+ raise typer.Exit(1)
40
+ resp.raise_for_status()
41
+ data = resp.json()
42
+
43
+ console.print(f"[green]Created organization: {data['slug']}[/]")
44
+
45
+
46
+ @org_app.command("list")
47
+ def list_orgs() -> None:
48
+ """List organizations you belong to."""
49
+ with httpx.Client() as client:
50
+ resp = client.get(
51
+ f"{_api_url()}/v1/orgs",
52
+ headers=_headers(),
53
+ )
54
+ resp.raise_for_status()
55
+ orgs = resp.json()
56
+
57
+ if not orgs:
58
+ console.print("You are not a member of any organizations.")
59
+ return
60
+
61
+ table = Table(title="Organizations")
62
+ table.add_column("Slug", style="cyan")
63
+ table.add_column("Role", style="green")
64
+
65
+ for org in orgs:
66
+ table.add_row(org.get("slug", ""), org.get("role", ""))
67
+
68
+ console.print(table)
69
+
70
+
71
+ @org_app.command("invite")
72
+ def invite_member(
73
+ org: str = typer.Argument(help="Organization slug"),
74
+ user: str = typer.Option(..., "--user", help="GitHub username to invite"),
75
+ role: str = typer.Option(
76
+ "member", "--role", help="Role: owner, admin, or member"
77
+ ),
78
+ ) -> None:
79
+ """Invite a user to an organization."""
80
+ with httpx.Client() as client:
81
+ resp = client.post(
82
+ f"{_api_url()}/v1/orgs/{org}/invites",
83
+ headers=_headers(),
84
+ json={"invitee_github_username": user, "role": role},
85
+ )
86
+ if resp.status_code == 404:
87
+ console.print(
88
+ f"[red]Error: Organization '{org}' not found.[/]"
89
+ )
90
+ raise typer.Exit(1)
91
+ if resp.status_code == 403:
92
+ console.print(
93
+ "[red]Error: You do not have permission to invite members.[/]"
94
+ )
95
+ raise typer.Exit(1)
96
+ resp.raise_for_status()
97
+ data = resp.json()
98
+
99
+ console.print(
100
+ f"[green]Invited @{user} to '{org}' as {role}. "
101
+ f"Invite ID: {data['id']}[/]"
102
+ )
103
+
104
+
105
+ @org_app.command("accept")
106
+ def accept_invite(
107
+ invite_id: str = typer.Argument(help="Invite ID to accept"),
108
+ ) -> None:
109
+ """Accept an organization invite."""
110
+ with httpx.Client() as client:
111
+ resp = client.post(
112
+ f"{_api_url()}/v1/invites/{invite_id}/accept",
113
+ headers=_headers(),
114
+ )
115
+ if resp.status_code == 404:
116
+ console.print(
117
+ f"[red]Error: Invite '{invite_id}' not found.[/]"
118
+ )
119
+ raise typer.Exit(1)
120
+ resp.raise_for_status()
121
+ data = resp.json()
122
+
123
+ console.print(
124
+ f"[green]Accepted invite. You are now a member of '{data['org_slug']}'.[/]"
125
+ )