tamarind-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.
@@ -0,0 +1,41 @@
1
+ name: Publish to PyPI
2
+
3
+ # Publishes tamarind-cli to PyPI on a GitHub Release (or manual dispatch).
4
+ #
5
+ # Uses PyPI Trusted Publishing (OIDC) — no API token in the repo. One-time setup
6
+ # on PyPI: add a trusted publisher for project `tamarind-cli` pointing at this
7
+ # repo + this workflow (`publish.yml`). If you prefer a token instead, delete the
8
+ # `permissions:`/OIDC bits and set a `PYPI_API_TOKEN` secret, then pass
9
+ # `password: ${{ secrets.PYPI_API_TOKEN }}` to the publish step.
10
+
11
+ on:
12
+ release:
13
+ types: [published]
14
+ workflow_dispatch:
15
+
16
+ jobs:
17
+ build:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ - uses: astral-sh/setup-uv@v5
22
+ - name: Build sdist + wheel
23
+ run: uv build
24
+ - name: Smoke-check the build
25
+ run: uv run --with dist/*.whl tamarind --version
26
+ - uses: actions/upload-artifact@v4
27
+ with:
28
+ name: dist
29
+ path: dist/
30
+
31
+ publish:
32
+ needs: build
33
+ runs-on: ubuntu-latest
34
+ permissions:
35
+ id-token: write # required for PyPI Trusted Publishing (OIDC)
36
+ steps:
37
+ - uses: actions/download-artifact@v4
38
+ with:
39
+ name: dist
40
+ path: dist/
41
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,11 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ build/
6
+ dist/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .coverage
10
+ htmlcov/
11
+ .tamarind/
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.4
2
+ Name: tamarind-cli
3
+ Version: 0.1.0
4
+ Summary: Command-line interface for the Tamarind Bio platform — submit, monitor, and download protein/molecule jobs from your terminal or an AI agent.
5
+ Project-URL: Homepage, https://tamarind.bio
6
+ Project-URL: Documentation, https://app.tamarind.bio/api-docs
7
+ Project-URL: Source, https://github.com/Tamarind-Bio/tamarind-cli
8
+ Author: Tamarind Bio
9
+ License: MIT
10
+ Keywords: agents,alphafold,bioinformatics,boltz,cli,protein
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: httpx>=0.27
13
+ Requires-Dist: pyyaml>=6.0
14
+ Requires-Dist: typer>=0.12
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest>=8.0; extra == 'dev'
17
+ Requires-Dist: respx>=0.21; extra == 'dev'
18
+ Requires-Dist: ruff>=0.5; extra == 'dev'
19
+ Description-Content-Type: text/markdown
20
+
21
+ # Tamarind CLI
22
+
23
+ Command-line interface for the [Tamarind Bio](https://tamarind.bio) platform.
24
+ Discover tools, submit and monitor protein / nucleic-acid / small-molecule jobs,
25
+ and download results — from your terminal, a script, CI, or an AI coding agent
26
+ (Claude Code, Codex, …).
27
+
28
+ The CLI is a thin client over the same API the [Tamarind MCP
29
+ server](https://mcp.tamarind.bio) uses, so the two stay in lockstep. See
30
+ [`docs/architecture.md`](docs/architecture.md) for how drift is prevented.
31
+
32
+ ## Install
33
+
34
+ Once published to PyPI:
35
+
36
+ ```bash
37
+ curl -fsSL https://install.tamarind.bio/cli/install.sh | sh
38
+ # or:
39
+ uv tool install tamarind-cli # or: pipx install tamarind-cli
40
+ ```
41
+
42
+ Before then (or to track the repo directly), install from git:
43
+
44
+ ```bash
45
+ uv tool install "git+https://github.com/Tamarind-Bio/tamarind-cli"
46
+ # or: pipx install "git+https://github.com/Tamarind-Bio/tamarind-cli"
47
+ ```
48
+
49
+ > Releasing: tag a GitHub Release and the [`publish.yml`](.github/workflows/publish.yml)
50
+ > workflow builds and uploads to PyPI via Trusted Publishing (configure the
51
+ > trusted publisher for `tamarind-cli` on PyPI first).
52
+
53
+ ## Authenticate
54
+
55
+ Get an API key from the Tamarind web app (Settings → API), then either:
56
+
57
+ ```bash
58
+ export TAMARIND_API_KEY="sk_..." # best for agents / CI
59
+ # or
60
+ tamarind auth login # stores it in ~/.tamarind/config.json
61
+ tamarind auth status
62
+ ```
63
+
64
+ ## Quickstart
65
+
66
+ ```bash
67
+ # 1. Find a tool
68
+ tamarind tools --function structure-prediction --modality protein
69
+ tamarind tools --search boltz
70
+
71
+ # 2. Inspect its parameters and grab a runnable example
72
+ tamarind schema boltz
73
+ tamarind schema boltz --example > job.yaml
74
+
75
+ # 3. Validate, then submit
76
+ tamarind validate boltz --input job.yaml
77
+ tamarind submit boltz --input job.yaml --name my-run --wait --download ./out
78
+
79
+ # 4. Monitor / fetch
80
+ tamarind jobs
81
+ tamarind status my-run
82
+ tamarind results my-run --download ./out
83
+ ```
84
+
85
+ Set individual fields inline instead of a file:
86
+
87
+ ```bash
88
+ tamarind submit boltz \
89
+ --set inputFormat=sequence \
90
+ --set sequence=MKTVRQERLKSIVRIL... \
91
+ --name quick-fold
92
+ ```
93
+
94
+ ## Output for agents
95
+
96
+ Every command emits JSON when stdout is not a TTY, or with `--json`. Exit codes
97
+ are stable: `0` ok, `3` auth, `4` not-found, `5` validation, `6` rate-limit.
98
+
99
+ ```bash
100
+ tamarind jobs --json | jq '.jobs[] | select(.JobStatus=="Running")'
101
+ ```
102
+
103
+ ## Commands
104
+
105
+ | Group | Commands |
106
+ |---|---|
107
+ | Discover | `tools`, `modalities`, `functions`, `schema` |
108
+ | Submit | `validate`, `submit`, `batch` |
109
+ | Monitor | `jobs`, `status`, `wait`, `results`, `logs` |
110
+ | Files | `files list`, `files upload`, `files delete`, `files folders` |
111
+ | Lifecycle | `cancel`, `delete` |
112
+ | Auth | `auth login`, `auth status`, `auth logout` |
113
+
114
+ Run `tamarind <command> --help` for full options.
115
+
116
+ ## Configuration
117
+
118
+ | Setting | Flag | Env var | Default |
119
+ |---|---|---|---|
120
+ | API key | `--api-key` | `TAMARIND_API_KEY` | — |
121
+ | Job API base | `--api-base` | `TAMARIND_API_BASE` | `https://app.tamarind.bio/api/` |
122
+ | Catalog base | `--catalog-base` | `TAMARIND_CATALOG_BASE` | `https://mcp.tamarind.bio` |
123
+ | Profile | `--profile` | `TAMARIND_PROFILE` | `default` |
124
+
125
+ Profiles (key + endpoints) are stored in `~/.tamarind/config.json`. Use a
126
+ profile to point at staging:
127
+
128
+ ```bash
129
+ tamarind --profile staging --api-base https://staging.tamarind.bio/api/ auth login
130
+ TAMARIND_PROFILE=staging tamarind tools
131
+ ```
@@ -0,0 +1,111 @@
1
+ # Tamarind CLI
2
+
3
+ Command-line interface for the [Tamarind Bio](https://tamarind.bio) platform.
4
+ Discover tools, submit and monitor protein / nucleic-acid / small-molecule jobs,
5
+ and download results — from your terminal, a script, CI, or an AI coding agent
6
+ (Claude Code, Codex, …).
7
+
8
+ The CLI is a thin client over the same API the [Tamarind MCP
9
+ server](https://mcp.tamarind.bio) uses, so the two stay in lockstep. See
10
+ [`docs/architecture.md`](docs/architecture.md) for how drift is prevented.
11
+
12
+ ## Install
13
+
14
+ Once published to PyPI:
15
+
16
+ ```bash
17
+ curl -fsSL https://install.tamarind.bio/cli/install.sh | sh
18
+ # or:
19
+ uv tool install tamarind-cli # or: pipx install tamarind-cli
20
+ ```
21
+
22
+ Before then (or to track the repo directly), install from git:
23
+
24
+ ```bash
25
+ uv tool install "git+https://github.com/Tamarind-Bio/tamarind-cli"
26
+ # or: pipx install "git+https://github.com/Tamarind-Bio/tamarind-cli"
27
+ ```
28
+
29
+ > Releasing: tag a GitHub Release and the [`publish.yml`](.github/workflows/publish.yml)
30
+ > workflow builds and uploads to PyPI via Trusted Publishing (configure the
31
+ > trusted publisher for `tamarind-cli` on PyPI first).
32
+
33
+ ## Authenticate
34
+
35
+ Get an API key from the Tamarind web app (Settings → API), then either:
36
+
37
+ ```bash
38
+ export TAMARIND_API_KEY="sk_..." # best for agents / CI
39
+ # or
40
+ tamarind auth login # stores it in ~/.tamarind/config.json
41
+ tamarind auth status
42
+ ```
43
+
44
+ ## Quickstart
45
+
46
+ ```bash
47
+ # 1. Find a tool
48
+ tamarind tools --function structure-prediction --modality protein
49
+ tamarind tools --search boltz
50
+
51
+ # 2. Inspect its parameters and grab a runnable example
52
+ tamarind schema boltz
53
+ tamarind schema boltz --example > job.yaml
54
+
55
+ # 3. Validate, then submit
56
+ tamarind validate boltz --input job.yaml
57
+ tamarind submit boltz --input job.yaml --name my-run --wait --download ./out
58
+
59
+ # 4. Monitor / fetch
60
+ tamarind jobs
61
+ tamarind status my-run
62
+ tamarind results my-run --download ./out
63
+ ```
64
+
65
+ Set individual fields inline instead of a file:
66
+
67
+ ```bash
68
+ tamarind submit boltz \
69
+ --set inputFormat=sequence \
70
+ --set sequence=MKTVRQERLKSIVRIL... \
71
+ --name quick-fold
72
+ ```
73
+
74
+ ## Output for agents
75
+
76
+ Every command emits JSON when stdout is not a TTY, or with `--json`. Exit codes
77
+ are stable: `0` ok, `3` auth, `4` not-found, `5` validation, `6` rate-limit.
78
+
79
+ ```bash
80
+ tamarind jobs --json | jq '.jobs[] | select(.JobStatus=="Running")'
81
+ ```
82
+
83
+ ## Commands
84
+
85
+ | Group | Commands |
86
+ |---|---|
87
+ | Discover | `tools`, `modalities`, `functions`, `schema` |
88
+ | Submit | `validate`, `submit`, `batch` |
89
+ | Monitor | `jobs`, `status`, `wait`, `results`, `logs` |
90
+ | Files | `files list`, `files upload`, `files delete`, `files folders` |
91
+ | Lifecycle | `cancel`, `delete` |
92
+ | Auth | `auth login`, `auth status`, `auth logout` |
93
+
94
+ Run `tamarind <command> --help` for full options.
95
+
96
+ ## Configuration
97
+
98
+ | Setting | Flag | Env var | Default |
99
+ |---|---|---|---|
100
+ | API key | `--api-key` | `TAMARIND_API_KEY` | — |
101
+ | Job API base | `--api-base` | `TAMARIND_API_BASE` | `https://app.tamarind.bio/api/` |
102
+ | Catalog base | `--catalog-base` | `TAMARIND_CATALOG_BASE` | `https://mcp.tamarind.bio` |
103
+ | Profile | `--profile` | `TAMARIND_PROFILE` | `default` |
104
+
105
+ Profiles (key + endpoints) are stored in `~/.tamarind/config.json`. Use a
106
+ profile to point at staging:
107
+
108
+ ```bash
109
+ tamarind --profile staging --api-base https://staging.tamarind.bio/api/ auth login
110
+ TAMARIND_PROFILE=staging tamarind tools
111
+ ```
@@ -0,0 +1,49 @@
1
+ # Architecture & no-drift design
2
+
3
+ The CLI, the [MCP server](https://mcp.tamarind.bio), and the web app are all
4
+ **thin clients** over the same platform. The CLI never re-implements business
5
+ logic; it only knows how to call two well-defined surfaces. This is what keeps
6
+ the CLI and the MCP from drifting as the platform evolves.
7
+
8
+ ## Two surfaces, two single sources of truth
9
+
10
+ ### 1. Job/file REST surface — source of truth: the OpenAPI spec
11
+
12
+ `submit`, `validate`, `batch`, `jobs`, `status`/`wait`, `results`, `files`,
13
+ `cancel`, `delete` map 1:1 onto operations in `openapi-mcp.yaml` — the same
14
+ spec the MCP server is generated from (`FastMCP.from_openapi`). The CLI's
15
+ [`rest.py`](../src/tamarind/rest.py) is a literal, logic-free mapping of that
16
+ spec. Because both clients derive from one spec, a contract test can fail CI if
17
+ they diverge.
18
+
19
+ These calls go directly to the job API (`https://app.tamarind.bio/api/`) with
20
+ the `x-api-key` header.
21
+
22
+ ### 2. Discovery surface — source of truth: a shared catalog module
23
+
24
+ `tools`, `modalities`, `functions`, and `schema` need per-org visibility logic
25
+ (which tools an account may see, per-parameter gating, example generation) that
26
+ must run server-side. So the CLI does **not** read the catalog database; it
27
+ calls the `/catalog/*` HTTP routes ([`catalog.py`](../src/tamarind/catalog.py)),
28
+ which return exactly the JSON the MCP's discovery tools return.
29
+
30
+ The MCP tools and the `/catalog/*` routes are served by the **same shared
31
+ implementation**, so a tool looks identical no matter which client you use.
32
+ Because the logic lives in one module, *where* discovery is hosted (the MCP host
33
+ today; potentially the main API or a dedicated service later) is a deployment
34
+ detail that can change without any client change and without drift.
35
+
36
+ ## Why not a single binary that re-encodes the API?
37
+
38
+ A from-scratch client in another language would re-encode the request shapes and
39
+ the catalog logic — two copies that drift the moment the platform changes.
40
+ Keeping the CLI a thin, spec-derived Python client that shares the OpenAPI
41
+ contract (and, server-side, the catalog module) with the MCP makes drift a
42
+ structural impossibility plus a CI-enforced check, rather than something to
43
+ remember.
44
+
45
+ ## Configuration indirection
46
+
47
+ Endpoints are configurable (`--api-base`, `--catalog-base`, profiles), so the
48
+ same binary points at prod or staging, and the discovery host can move later
49
+ without a new release.
@@ -0,0 +1,38 @@
1
+ #!/bin/sh
2
+ # Tamarind CLI installer.
3
+ #
4
+ # curl -fsSL https://install.tamarind.bio/cli/install.sh | sh
5
+ #
6
+ # Installs the `tamarind` command into an isolated environment using whatever
7
+ # Python tooling is available (uv > pipx > pip --user). Override the package
8
+ # source with TAMARIND_CLI_SPEC (e.g. a local path or a git URL).
9
+ set -eu
10
+
11
+ SPEC="${TAMARIND_CLI_SPEC:-tamarind-cli}"
12
+
13
+ say() { printf '\033[0;36m%s\033[0m\n' "$*"; }
14
+ warn() { printf '\033[0;33m%s\033[0m\n' "$*" >&2; }
15
+ die() { printf '\033[0;31merror: %s\033[0m\n' "$*" >&2; exit 1; }
16
+
17
+ have() { command -v "$1" >/dev/null 2>&1; }
18
+
19
+ if have uv; then
20
+ say "Installing $SPEC with uv…"
21
+ uv tool install --force "$SPEC"
22
+ elif have pipx; then
23
+ say "Installing $SPEC with pipx…"
24
+ pipx install --force "$SPEC"
25
+ elif have python3; then
26
+ say "Installing $SPEC with pip (--user)…"
27
+ python3 -m pip install --user --upgrade "$SPEC"
28
+ else
29
+ die "Need uv, pipx, or python3 on PATH. Install uv: https://docs.astral.sh/uv/"
30
+ fi
31
+
32
+ if have tamarind; then
33
+ say "Installed: $(tamarind --version)"
34
+ say "Next: export TAMARIND_API_KEY=... (or run 'tamarind auth login'), then 'tamarind tools'."
35
+ else
36
+ warn "Installed, but 'tamarind' is not on PATH yet."
37
+ warn "Add your tool bin dir to PATH (uv: 'uv tool update-shell'; pipx: 'pipx ensurepath')."
38
+ fi
@@ -0,0 +1,44 @@
1
+ [project]
2
+ name = "tamarind-cli"
3
+ version = "0.1.0"
4
+ description = "Command-line interface for the Tamarind Bio platform — submit, monitor, and download protein/molecule jobs from your terminal or an AI agent."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "Tamarind Bio" }]
9
+ keywords = ["bioinformatics", "protein", "alphafold", "boltz", "cli", "agents"]
10
+ dependencies = [
11
+ "httpx>=0.27",
12
+ "typer>=0.12",
13
+ "PyYAML>=6.0",
14
+ ]
15
+
16
+ [project.urls]
17
+ Homepage = "https://tamarind.bio"
18
+ Documentation = "https://app.tamarind.bio/api-docs"
19
+ Source = "https://github.com/Tamarind-Bio/tamarind-cli"
20
+
21
+ [project.scripts]
22
+ tamarind = "tamarind.cli.main:run"
23
+
24
+ [project.optional-dependencies]
25
+ dev = [
26
+ "pytest>=8.0",
27
+ "respx>=0.21",
28
+ "ruff>=0.5",
29
+ ]
30
+
31
+ [build-system]
32
+ requires = ["hatchling"]
33
+ build-backend = "hatchling.build"
34
+
35
+ [tool.hatch.build.targets.wheel]
36
+ packages = ["src/tamarind"]
37
+
38
+ [tool.pytest.ini_options]
39
+ testpaths = ["tests"]
40
+ addopts = "-q"
41
+
42
+ [tool.ruff]
43
+ line-length = 100
44
+ target-version = "py310"
@@ -0,0 +1,16 @@
1
+ """Tamarind Bio CLI and Python client.
2
+
3
+ This package is a thin client over the Tamarind platform. Two surfaces:
4
+
5
+ - REST passthrough (``tamarind.rest``): submit/validate/batch, jobs, result,
6
+ files, cancel, delete — these hit the Tamarind API directly with an API key.
7
+ The request/response contract is the same OpenAPI spec the Tamarind MCP server
8
+ is built from, so the CLI and the MCP cannot drift on this surface.
9
+
10
+ - Discovery (``tamarind.catalog``): tools, schema, modalities, functions. The
11
+ catalog lives behind per-org visibility logic that runs server-side, so the
12
+ CLI consumes it over HTTP (the ``/catalog/*`` routes) rather than reading the
13
+ database directly.
14
+ """
15
+
16
+ __version__ = "0.1.0"
@@ -0,0 +1,70 @@
1
+ """Discovery / catalog client.
2
+
3
+ The tool catalog and per-tool schemas are gated by per-org visibility logic
4
+ that runs server-side, so the CLI consumes them over HTTP from the catalog
5
+ service (the ``/catalog/*`` routes) rather than reading the database directly.
6
+
7
+ These routes return exactly the JSON the MCP's discovery tools return
8
+ (``getAvailableTools``, ``listModalities``, ``listTags``, ``getJobSchema``),
9
+ because both are served by the same shared implementation. So whatever a tool
10
+ looks like in the MCP, it looks identical here.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Any
16
+
17
+ from .http import HTTPClient
18
+
19
+ CATALOG_PREFIX = "catalog"
20
+
21
+
22
+ def list_tools(
23
+ client: HTTPClient,
24
+ *,
25
+ modality: str | None = None,
26
+ function: str | None = None,
27
+ search: str | None = None,
28
+ custom: bool | None = None,
29
+ ) -> dict:
30
+ """GET /catalog/tools — the filtered tool catalog (mirrors getAvailableTools)."""
31
+ params = {
32
+ "modality": modality,
33
+ "function": function,
34
+ "search": search,
35
+ "custom": "true" if custom else None,
36
+ }
37
+ return client.get_json(f"{CATALOG_PREFIX}/tools", params=params)
38
+
39
+
40
+ def list_modalities(client: HTTPClient) -> dict:
41
+ """GET /catalog/modalities — molecule types you can filter by."""
42
+ return client.get_json(f"{CATALOG_PREFIX}/modalities")
43
+
44
+
45
+ def list_functions(client: HTTPClient) -> dict:
46
+ """GET /catalog/functions — functions (tags) you can filter by."""
47
+ return client.get_json(f"{CATALOG_PREFIX}/functions")
48
+
49
+
50
+ def get_schema(client: HTTPClient, job_type: str) -> dict:
51
+ """GET /catalog/tools/{jobType}/schema — full parameter schema + example job."""
52
+ return client.get_json(f"{CATALOG_PREFIX}/tools/{job_type}/schema")
53
+
54
+
55
+ # -- helpers for rendering / example extraction ---------------------------
56
+
57
+
58
+ def example_settings(schema: dict[str, Any]) -> dict[str, Any]:
59
+ """Pull a runnable ``settings`` dict out of a schema's exampleJob, if present."""
60
+ example = schema.get("exampleJob") or {}
61
+ return dict(example.get("settings") or {})
62
+
63
+
64
+ def required_param_names(schema: dict[str, Any]) -> list[str]:
65
+ """Names of parameters marked required (top-level; ignores task-gated ones)."""
66
+ out = []
67
+ for p in schema.get("parameters", []):
68
+ if p.get("required") and p.get("name"):
69
+ out.append(p["name"])
70
+ return out
@@ -0,0 +1 @@
1
+ """Typer command-line interface for Tamarind."""
@@ -0,0 +1 @@
1
+ """CLI command groups."""
@@ -0,0 +1,90 @@
1
+ """`tamarind auth` — credential management."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+
7
+ import typer
8
+
9
+ from ... import rest
10
+ from ...config import mask_key, save_profile
11
+ from ...errors import AuthError
12
+ from ...http import HTTPClient
13
+ from .. import output
14
+
15
+ app = typer.Typer(no_args_is_help=True)
16
+
17
+
18
+ def _check_key(api_base: str, api_key: str) -> bool:
19
+ """Return True if the key authenticates against the job API."""
20
+ with HTTPClient(api_base, api_key) as client:
21
+ try:
22
+ rest.get_jobs(client, limit=1)
23
+ return True
24
+ except AuthError:
25
+ return False
26
+
27
+
28
+ @app.command()
29
+ def login(
30
+ ctx: typer.Context,
31
+ api_key: Optional[str] = typer.Option(
32
+ None, "--api-key", help="API key. If omitted, you'll be prompted.", show_default=False
33
+ ),
34
+ no_verify: bool = typer.Option(False, "--no-verify", help="Skip verifying the key."),
35
+ ) -> None:
36
+ """Store an API key in the current profile (~/.tamarind/config.json).
37
+
38
+ Get a key from https://app.tamarind.bio (Settings → API), or set
39
+ TAMARIND_API_KEY in the environment to skip storing one.
40
+ """
41
+ state = ctx.obj
42
+ cfg = state.config()
43
+ key = api_key or typer.prompt("Tamarind API key", hide_input=True)
44
+ key = key.strip()
45
+
46
+ if not no_verify and not _check_key(cfg.api_base, key):
47
+ raise AuthError("That API key was rejected by the API. Not saved.")
48
+
49
+ save_profile(cfg.profile, api_key=key)
50
+ output.emit(
51
+ {"ok": True, "profile": cfg.profile, "verified": not no_verify},
52
+ state.output,
53
+ human=f"Saved API key to profile '{cfg.profile}'.",
54
+ )
55
+
56
+
57
+ @app.command()
58
+ def status(ctx: typer.Context) -> None:
59
+ """Show the active profile, endpoints, and whether the key works."""
60
+ state = ctx.obj
61
+ cfg = state.config()
62
+ verified = cfg.has_key and _check_key(cfg.api_base, cfg.api_key)
63
+ result = {
64
+ "profile": cfg.profile,
65
+ "apiKey": mask_key(cfg.api_key),
66
+ "hasKey": cfg.has_key,
67
+ "verified": verified,
68
+ "apiBase": cfg.api_base,
69
+ "catalogBase": cfg.catalog_base,
70
+ }
71
+ human = (
72
+ f"profile: {cfg.profile}\n"
73
+ f"api key: {mask_key(cfg.api_key)} ({'verified' if verified else 'not verified'})\n"
74
+ f"job api: {cfg.api_base}\n"
75
+ f"catalog api: {cfg.catalog_base}"
76
+ )
77
+ output.emit(result, state.output, human=human)
78
+
79
+
80
+ @app.command()
81
+ def logout(ctx: typer.Context) -> None:
82
+ """Remove the stored API key from the current profile."""
83
+ state = ctx.obj
84
+ cfg = state.config()
85
+ save_profile(cfg.profile, api_key="", make_current=False)
86
+ output.emit(
87
+ {"ok": True, "profile": cfg.profile},
88
+ state.output,
89
+ human=f"Cleared API key for profile '{cfg.profile}'.",
90
+ )