expedait-cli 0.2.1__tar.gz → 0.3.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 (48) hide show
  1. expedait_cli-0.3.0/CHANGELOG.md +35 -0
  2. expedait_cli-0.3.0/PKG-INFO +195 -0
  3. expedait_cli-0.3.0/README.md +184 -0
  4. {expedait_cli-0.2.1 → expedait_cli-0.3.0}/expedait_cli/auth.py +30 -2
  5. expedait_cli-0.3.0/expedait_cli/client.py +184 -0
  6. expedait_cli-0.3.0/expedait_cli/commands/comments.py +146 -0
  7. expedait_cli-0.3.0/expedait_cli/commands/context_cmd.py +40 -0
  8. expedait_cli-0.3.0/expedait_cli/commands/deliverables.py +245 -0
  9. expedait_cli-0.3.0/expedait_cli/commands/init_cmd.py +77 -0
  10. expedait_cli-0.3.0/expedait_cli/commands/objectives.py +62 -0
  11. {expedait_cli-0.2.1 → expedait_cli-0.3.0}/expedait_cli/commands/projects.py +11 -5
  12. expedait_cli-0.3.0/expedait_cli/commands/review.py +81 -0
  13. {expedait_cli-0.2.1 → expedait_cli-0.3.0}/expedait_cli/main.py +12 -2
  14. expedait_cli-0.3.0/expedait_cli/settings.py +31 -0
  15. {expedait_cli-0.2.1 → expedait_cli-0.3.0}/pyproject.toml +3 -1
  16. {expedait_cli-0.2.1 → expedait_cli-0.3.0}/tests/test_auth.py +30 -3
  17. {expedait_cli-0.2.1 → expedait_cli-0.3.0}/tests/test_client.py +26 -2
  18. expedait_cli-0.3.0/tests/test_commands/test_comments.py +122 -0
  19. expedait_cli-0.3.0/tests/test_commands/test_context.py +32 -0
  20. expedait_cli-0.3.0/tests/test_commands/test_deliverables.py +141 -0
  21. expedait_cli-0.3.0/tests/test_commands/test_init_cmd.py +161 -0
  22. expedait_cli-0.3.0/tests/test_commands/test_objectives.py +49 -0
  23. {expedait_cli-0.2.1 → expedait_cli-0.3.0}/tests/test_commands/test_projects.py +32 -0
  24. expedait_cli-0.3.0/tests/test_commands/test_review.py +62 -0
  25. expedait_cli-0.3.0/tests/test_settings.py +33 -0
  26. {expedait_cli-0.2.1 → expedait_cli-0.3.0}/uv.lock +1 -1
  27. expedait_cli-0.2.1/PKG-INFO +0 -153
  28. expedait_cli-0.2.1/README.md +0 -142
  29. expedait_cli-0.2.1/expedait_cli/client.py +0 -130
  30. expedait_cli-0.2.1/expedait_cli/commands/comments.py +0 -102
  31. expedait_cli-0.2.1/expedait_cli/commands/pages.py +0 -104
  32. expedait_cli-0.2.1/tests/test_commands/test_comments.py +0 -138
  33. expedait_cli-0.2.1/tests/test_commands/test_pages.py +0 -121
  34. {expedait_cli-0.2.1 → expedait_cli-0.3.0}/.github/workflows/ci.yml +0 -0
  35. {expedait_cli-0.2.1 → expedait_cli-0.3.0}/.github/workflows/publish.yml +0 -0
  36. {expedait_cli-0.2.1 → expedait_cli-0.3.0}/.gitignore +0 -0
  37. {expedait_cli-0.2.1 → expedait_cli-0.3.0}/CLAUDE.md +0 -0
  38. {expedait_cli-0.2.1 → expedait_cli-0.3.0}/LICENSE +0 -0
  39. {expedait_cli-0.2.1 → expedait_cli-0.3.0}/expedait_cli/__init__.py +0 -0
  40. {expedait_cli-0.2.1 → expedait_cli-0.3.0}/expedait_cli/commands/__init__.py +0 -0
  41. {expedait_cli-0.2.1 → expedait_cli-0.3.0}/expedait_cli/commands/auth_cmd.py +0 -0
  42. {expedait_cli-0.2.1 → expedait_cli-0.3.0}/expedait_cli/config.py +0 -0
  43. {expedait_cli-0.2.1 → expedait_cli-0.3.0}/expedait_cli/formatters.py +0 -0
  44. {expedait_cli-0.2.1 → expedait_cli-0.3.0}/tests/__init__.py +0 -0
  45. {expedait_cli-0.2.1 → expedait_cli-0.3.0}/tests/conftest.py +0 -0
  46. {expedait_cli-0.2.1 → expedait_cli-0.3.0}/tests/test_commands/__init__.py +0 -0
  47. {expedait_cli-0.2.1 → expedait_cli-0.3.0}/tests/test_commands/test_auth_cmd.py +0 -0
  48. {expedait_cli-0.2.1 → expedait_cli-0.3.0}/tests/test_config.py +0 -0
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ ## 0.3.0
4
+
5
+ Adapt the CLI to the product's four-primitive domain model (objectives,
6
+ deliverables, context, review).
7
+
8
+ ### Added
9
+ - `deliverables` command group (`list`, `get`, `inspect`, `download`) — the
10
+ rename of `pages`, pointed at `/api/v1/deliverables/...`.
11
+ - `deliverables get --include` — comma-separated section reads (`meta`,
12
+ `content`, `template`, `requirements`, `writer_instructions`, `dependencies`,
13
+ `external_context`, `score`, `comments`, `versions`), defaulting to `content`.
14
+ `meta` surfaces `parent_deliverable_id`.
15
+ - `objectives overview DELIVERABLE_ID` — objective metadata plus its full
16
+ descendant tree.
17
+ - `context get DELIVERABLE_ID` — read-only LLM context snapshot for a
18
+ deliverable.
19
+ - `review` command group: `review issues DELIVERABLE_ID [--state open|muted|all]`
20
+ and `review mute ISSUE_ID [--note TEXT] [--unmute]`.
21
+ - `comments create --agent-run-id` to link a comment to a build run.
22
+ - `expedait-cli` console-script alias so `uvx expedait-cli …` keeps working.
23
+
24
+ ### Changed
25
+ - `comments create` now resolves anchor offsets from the deliverable content;
26
+ only `--text` and `--selected-text` are required. `--start-offset` /
27
+ `--end-offset` remain available as explicit overrides.
28
+ - Renamed `comments create --source-page-id` → `--source-deliverable-id`
29
+ (payload field `source_page_id` → `source_deliverable_id`).
30
+ - `comments resolve` / `comments delete` now use the deliverable-scoped routes
31
+ `/api/v1/deliverables/{id}/comments/{comment_id}`.
32
+
33
+ ### Deprecated
34
+ - The `pages` command group. `expedait pages …` still works for one release
35
+ (warns and forwards to `deliverables`) and will then be removed.
@@ -0,0 +1,195 @@
1
+ Metadata-Version: 2.4
2
+ Name: expedait-cli
3
+ Version: 0.3.0
4
+ Summary: CLI for Expedait project management — download specs, post comments
5
+ License-Expression: Apache-2.0
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.11
8
+ Requires-Dist: click>=8.1
9
+ Requires-Dist: httpx>=0.27
10
+ Description-Content-Type: text/markdown
11
+
12
+ # Expedait CLI
13
+
14
+ [![PyPI](https://img.shields.io/pypi/v/expedait-cli)](https://pypi.org/project/expedait-cli/)
15
+ [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
16
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
17
+
18
+ CLI for [Expedait](https://expedait.org) — lets AI coding agents download project specs and post comments via the Expedait API.
19
+
20
+ ## The model
21
+
22
+ Expedait organizes specs around four primitives:
23
+
24
+ - **Objectives** — top-level goals. An objective is itself a deliverable that nests child deliverables beneath it (`parent_deliverable_id`).
25
+ - **Deliverables** — the individual spec documents (formerly "pages").
26
+ - **Context** — the assembled LLM context for one deliverable: dependency deliverables, linked external sources, uploaded files, and aggregate sizes.
27
+ - **Review** — scoring findings raised on a deliverable: severity, description, the criteria that flagged them, and anchor offsets.
28
+
29
+ ## Usage
30
+
31
+ ### Run with `uvx` (recommended)
32
+
33
+ No installation needed — run directly:
34
+
35
+ ```bash
36
+ uvx expedait-cli auth login
37
+ uvx expedait-cli projects list
38
+ uvx expedait-cli projects download 1
39
+ ```
40
+
41
+ ### Add as a dev dependency
42
+
43
+ If your AI agent needs it available in the project environment:
44
+
45
+ ```bash
46
+ uv add --group dev expedait-cli
47
+ ```
48
+
49
+ Then reference it in your agent configuration (e.g. `CLAUDE.md`, `.cursor/rules`, etc.).
50
+
51
+ ## Project Setup
52
+
53
+ After authenticating, run `init` inside your project directory to store your tenant and project settings locally:
54
+
55
+ ```bash
56
+ uvx expedait-cli init
57
+ ```
58
+
59
+ This creates `.expedait/settings.json` with your `tenant_id` and `project_id`. Add `.expedait/` to your `.gitignore`.
60
+
61
+ Once initialized, commands that need a project ID will resolve it automatically. Downloads default to `.expedait/context/`:
62
+
63
+ ```bash
64
+ expedait projects download # downloads to .expedait/context/
65
+ expedait deliverables list # no --project-id needed
66
+ expedait deliverables download 42 # downloads to .expedait/context/
67
+ ```
68
+
69
+ **Resolution order for tenant/project:** CLI flag > env var > `.expedait/settings.json` > `~/.expedait/config.json`.
70
+
71
+ ## Authentication
72
+
73
+ ### Interactive login
74
+
75
+ ```bash
76
+ uvx expedait-cli auth login
77
+ ```
78
+
79
+ Prompts for login method (SSO or email/password). Stores credentials in `~/.expedait/config.json`.
80
+
81
+ ### Environment variables (CI / agents)
82
+
83
+ ```bash
84
+ export EXPEDAIT_TOKEN="your-jwt-token"
85
+ export EXPEDAIT_API_URL="https://your-instance.expedait.org"
86
+ export EXPEDAIT_TENANT_ID=1
87
+ ```
88
+
89
+ **Token resolution order:** `EXPEDAIT_TOKEN` env var > `~/.expedait/config.json` > error.
90
+
91
+ ## Commands
92
+
93
+ ### Auth
94
+
95
+ ```bash
96
+ expedait auth login # Interactive login
97
+ expedait auth status # Show current user and tenant
98
+ expedait auth logout # Clear stored credentials
99
+ ```
100
+
101
+ ### Projects
102
+
103
+ ```bash
104
+ expedait projects list # List all projects
105
+ expedait projects get PROJECT_ID # Get project details
106
+ expedait projects download PROJECT_ID # Extract all deliverables to .expedait/context/
107
+ expedait projects download PROJECT_ID --output-dir ./specs # Extract to a custom directory
108
+ ```
109
+
110
+ ### Deliverables
111
+
112
+ ```bash
113
+ expedait deliverables list --project-id PROJECT_ID # List deliverables in a project
114
+ expedait deliverables get DELIVERABLE_ID # Print deliverable markdown content
115
+ expedait deliverables get DELIVERABLE_ID --include meta,content,dependencies,score
116
+ expedait deliverables inspect DELIVERABLE_ID # Full context (content + comments + deps + lock)
117
+ expedait deliverables download DELIVERABLE_ID # Extract to .expedait/context/
118
+ ```
119
+
120
+ `--include` accepts a comma-separated subset of: `meta`, `content`, `template`,
121
+ `requirements`, `writer_instructions`, `dependencies`, `external_context`,
122
+ `score`, `comments`, `versions`. It defaults to `content`. `meta` surfaces
123
+ `parent_deliverable_id` (non-null ⇒ this deliverable is a child nested under an
124
+ objective).
125
+
126
+ ### Objectives
127
+
128
+ ```bash
129
+ expedait objectives overview DELIVERABLE_ID # Objective metadata + full descendant tree
130
+ ```
131
+
132
+ ### Context
133
+
134
+ ```bash
135
+ expedait context get DELIVERABLE_ID # The LLM context snapshot for one deliverable
136
+ ```
137
+
138
+ ### Review
139
+
140
+ ```bash
141
+ expedait review issues DELIVERABLE_ID # List scoring findings (default: all)
142
+ expedait review issues DELIVERABLE_ID --state open # Only open findings
143
+ expedait review mute ISSUE_ID --note "by design" # Mute a finding
144
+ expedait review mute ISSUE_ID --unmute # Unmute a finding
145
+ ```
146
+
147
+ ### Comments
148
+
149
+ ```bash
150
+ expedait comments list DELIVERABLE_ID # List comments on a deliverable
151
+ expedait comments create DELIVERABLE_ID \ # Create a comment (offsets resolved automatically)
152
+ --text "Comment content" \
153
+ --selected-text "text from the deliverable" \
154
+ --source-deliverable-id 5 # Optional: agent's source deliverable
155
+ expedait comments resolve DELIVERABLE_ID COMMENT_ID # Mark as resolved
156
+ expedait comments delete DELIVERABLE_ID COMMENT_ID # Delete a comment
157
+ ```
158
+
159
+ Only `--text` and `--selected-text` are required; the CLI locates the selected
160
+ text in the deliverable to compute anchor offsets. Pass `--start-offset` and
161
+ `--end-offset` to anchor explicitly (e.g. when the selected text appears more
162
+ than once).
163
+
164
+ ### Global Options
165
+
166
+ ```bash
167
+ expedait --api-url https://host:8000 ... # Override API URL
168
+ expedait --tenant-id 2 ... # Override tenant
169
+ expedait --format json ... # Force JSON output
170
+ expedait --format text ... # Force human-readable output
171
+ expedait --version # Show version
172
+ ```
173
+
174
+ Output format defaults to `text` when connected to a terminal, `json` when piped.
175
+
176
+ > **Migration note:** the `pages` command group has been renamed to
177
+ > `deliverables`. `expedait pages …` still works for one release (it warns and
178
+ > forwards) but will be removed.
179
+
180
+ ## Agent Skills
181
+
182
+ For step-by-step guides on using the CLI from AI coding agents, see [expedait-skills](https://github.com/Expedait/expedait-skills).
183
+
184
+ ## Development
185
+
186
+ ```bash
187
+ git clone https://github.com/Expedait/expedait-cli.git
188
+ cd expedait-cli
189
+ uv sync --group dev
190
+ uv run python -m pytest
191
+ ```
192
+
193
+ ## License
194
+
195
+ [Apache License 2.0](LICENSE)
@@ -0,0 +1,184 @@
1
+ # Expedait CLI
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/expedait-cli)](https://pypi.org/project/expedait-cli/)
4
+ [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
5
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
6
+
7
+ CLI for [Expedait](https://expedait.org) — lets AI coding agents download project specs and post comments via the Expedait API.
8
+
9
+ ## The model
10
+
11
+ Expedait organizes specs around four primitives:
12
+
13
+ - **Objectives** — top-level goals. An objective is itself a deliverable that nests child deliverables beneath it (`parent_deliverable_id`).
14
+ - **Deliverables** — the individual spec documents (formerly "pages").
15
+ - **Context** — the assembled LLM context for one deliverable: dependency deliverables, linked external sources, uploaded files, and aggregate sizes.
16
+ - **Review** — scoring findings raised on a deliverable: severity, description, the criteria that flagged them, and anchor offsets.
17
+
18
+ ## Usage
19
+
20
+ ### Run with `uvx` (recommended)
21
+
22
+ No installation needed — run directly:
23
+
24
+ ```bash
25
+ uvx expedait-cli auth login
26
+ uvx expedait-cli projects list
27
+ uvx expedait-cli projects download 1
28
+ ```
29
+
30
+ ### Add as a dev dependency
31
+
32
+ If your AI agent needs it available in the project environment:
33
+
34
+ ```bash
35
+ uv add --group dev expedait-cli
36
+ ```
37
+
38
+ Then reference it in your agent configuration (e.g. `CLAUDE.md`, `.cursor/rules`, etc.).
39
+
40
+ ## Project Setup
41
+
42
+ After authenticating, run `init` inside your project directory to store your tenant and project settings locally:
43
+
44
+ ```bash
45
+ uvx expedait-cli init
46
+ ```
47
+
48
+ This creates `.expedait/settings.json` with your `tenant_id` and `project_id`. Add `.expedait/` to your `.gitignore`.
49
+
50
+ Once initialized, commands that need a project ID will resolve it automatically. Downloads default to `.expedait/context/`:
51
+
52
+ ```bash
53
+ expedait projects download # downloads to .expedait/context/
54
+ expedait deliverables list # no --project-id needed
55
+ expedait deliverables download 42 # downloads to .expedait/context/
56
+ ```
57
+
58
+ **Resolution order for tenant/project:** CLI flag > env var > `.expedait/settings.json` > `~/.expedait/config.json`.
59
+
60
+ ## Authentication
61
+
62
+ ### Interactive login
63
+
64
+ ```bash
65
+ uvx expedait-cli auth login
66
+ ```
67
+
68
+ Prompts for login method (SSO or email/password). Stores credentials in `~/.expedait/config.json`.
69
+
70
+ ### Environment variables (CI / agents)
71
+
72
+ ```bash
73
+ export EXPEDAIT_TOKEN="your-jwt-token"
74
+ export EXPEDAIT_API_URL="https://your-instance.expedait.org"
75
+ export EXPEDAIT_TENANT_ID=1
76
+ ```
77
+
78
+ **Token resolution order:** `EXPEDAIT_TOKEN` env var > `~/.expedait/config.json` > error.
79
+
80
+ ## Commands
81
+
82
+ ### Auth
83
+
84
+ ```bash
85
+ expedait auth login # Interactive login
86
+ expedait auth status # Show current user and tenant
87
+ expedait auth logout # Clear stored credentials
88
+ ```
89
+
90
+ ### Projects
91
+
92
+ ```bash
93
+ expedait projects list # List all projects
94
+ expedait projects get PROJECT_ID # Get project details
95
+ expedait projects download PROJECT_ID # Extract all deliverables to .expedait/context/
96
+ expedait projects download PROJECT_ID --output-dir ./specs # Extract to a custom directory
97
+ ```
98
+
99
+ ### Deliverables
100
+
101
+ ```bash
102
+ expedait deliverables list --project-id PROJECT_ID # List deliverables in a project
103
+ expedait deliverables get DELIVERABLE_ID # Print deliverable markdown content
104
+ expedait deliverables get DELIVERABLE_ID --include meta,content,dependencies,score
105
+ expedait deliverables inspect DELIVERABLE_ID # Full context (content + comments + deps + lock)
106
+ expedait deliverables download DELIVERABLE_ID # Extract to .expedait/context/
107
+ ```
108
+
109
+ `--include` accepts a comma-separated subset of: `meta`, `content`, `template`,
110
+ `requirements`, `writer_instructions`, `dependencies`, `external_context`,
111
+ `score`, `comments`, `versions`. It defaults to `content`. `meta` surfaces
112
+ `parent_deliverable_id` (non-null ⇒ this deliverable is a child nested under an
113
+ objective).
114
+
115
+ ### Objectives
116
+
117
+ ```bash
118
+ expedait objectives overview DELIVERABLE_ID # Objective metadata + full descendant tree
119
+ ```
120
+
121
+ ### Context
122
+
123
+ ```bash
124
+ expedait context get DELIVERABLE_ID # The LLM context snapshot for one deliverable
125
+ ```
126
+
127
+ ### Review
128
+
129
+ ```bash
130
+ expedait review issues DELIVERABLE_ID # List scoring findings (default: all)
131
+ expedait review issues DELIVERABLE_ID --state open # Only open findings
132
+ expedait review mute ISSUE_ID --note "by design" # Mute a finding
133
+ expedait review mute ISSUE_ID --unmute # Unmute a finding
134
+ ```
135
+
136
+ ### Comments
137
+
138
+ ```bash
139
+ expedait comments list DELIVERABLE_ID # List comments on a deliverable
140
+ expedait comments create DELIVERABLE_ID \ # Create a comment (offsets resolved automatically)
141
+ --text "Comment content" \
142
+ --selected-text "text from the deliverable" \
143
+ --source-deliverable-id 5 # Optional: agent's source deliverable
144
+ expedait comments resolve DELIVERABLE_ID COMMENT_ID # Mark as resolved
145
+ expedait comments delete DELIVERABLE_ID COMMENT_ID # Delete a comment
146
+ ```
147
+
148
+ Only `--text` and `--selected-text` are required; the CLI locates the selected
149
+ text in the deliverable to compute anchor offsets. Pass `--start-offset` and
150
+ `--end-offset` to anchor explicitly (e.g. when the selected text appears more
151
+ than once).
152
+
153
+ ### Global Options
154
+
155
+ ```bash
156
+ expedait --api-url https://host:8000 ... # Override API URL
157
+ expedait --tenant-id 2 ... # Override tenant
158
+ expedait --format json ... # Force JSON output
159
+ expedait --format text ... # Force human-readable output
160
+ expedait --version # Show version
161
+ ```
162
+
163
+ Output format defaults to `text` when connected to a terminal, `json` when piped.
164
+
165
+ > **Migration note:** the `pages` command group has been renamed to
166
+ > `deliverables`. `expedait pages …` still works for one release (it warns and
167
+ > forwards) but will be removed.
168
+
169
+ ## Agent Skills
170
+
171
+ For step-by-step guides on using the CLI from AI coding agents, see [expedait-skills](https://github.com/Expedait/expedait-skills).
172
+
173
+ ## Development
174
+
175
+ ```bash
176
+ git clone https://github.com/Expedait/expedait-cli.git
177
+ cd expedait-cli
178
+ uv sync --group dev
179
+ uv run python -m pytest
180
+ ```
181
+
182
+ ## License
183
+
184
+ [Apache License 2.0](LICENSE)
@@ -7,6 +7,7 @@ import os
7
7
  import click
8
8
 
9
9
  from .config import load_config
10
+ from .settings import load_settings
10
11
 
11
12
 
12
13
  def resolve_token(config_path=None) -> str:
@@ -41,15 +42,42 @@ def resolve_api_url(explicit: str | None = None, config_path=None) -> str:
41
42
  return "https://app.expedait.org"
42
43
 
43
44
 
44
- def resolve_tenant_id(explicit: int | None = None, config_path=None) -> int | None:
45
- """Return tenant ID from flag > env > config."""
45
+ def resolve_tenant_id(
46
+ explicit: int | None = None,
47
+ config_path=None,
48
+ settings_path=None,
49
+ ) -> int | None:
50
+ """Return tenant ID from flag > env > local settings > config."""
46
51
  if explicit is not None:
47
52
  return explicit
48
53
  env = os.environ.get("EXPEDAIT_TENANT_ID")
49
54
  if env:
50
55
  return int(env)
56
+ # Local project settings
57
+ settings = load_settings(settings_path)
58
+ tid = settings.get("tenant_id")
59
+ if tid is not None:
60
+ return int(tid)
61
+ # Global config
51
62
  cfg = load_config(config_path)
52
63
  tid = cfg.get("tenant_id")
53
64
  if tid is not None:
54
65
  return int(tid)
55
66
  return None
67
+
68
+
69
+ def resolve_project_id(
70
+ explicit: int | None = None,
71
+ settings_path=None,
72
+ ) -> int | None:
73
+ """Return project ID from flag > env > local settings."""
74
+ if explicit is not None:
75
+ return explicit
76
+ env = os.environ.get("EXPEDAIT_PROJECT_ID")
77
+ if env:
78
+ return int(env)
79
+ settings = load_settings(settings_path)
80
+ pid = settings.get("project_id")
81
+ if pid is not None:
82
+ return int(pid)
83
+ return None
@@ -0,0 +1,184 @@
1
+ """HTTP client wrapper for Expedait API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import click
8
+ import httpx
9
+
10
+
11
+ class ExpedaitClient:
12
+ """Thin wrapper around httpx with auth and tenant headers."""
13
+
14
+ def __init__(self, api_url: str, token: str, tenant_id: int | None = None):
15
+ headers: dict[str, str] = {"Authorization": f"Bearer {token}"}
16
+ if tenant_id is not None:
17
+ headers["X-Active-Tenant-Id"] = str(tenant_id)
18
+ # follow_redirects: collection endpoints are mounted at "/", so the
19
+ # API 307-redirects e.g. /api/v1/deliverables -> /api/v1/deliverables/.
20
+ self._http = httpx.Client(
21
+ base_url=api_url, headers=headers, timeout=30.0, follow_redirects=True,
22
+ )
23
+
24
+ def close(self) -> None:
25
+ self._http.close()
26
+
27
+ # -- helpers ----------------------------------------------------------
28
+
29
+ def _check(self, resp: httpx.Response) -> None:
30
+ if resp.status_code == 401:
31
+ raise click.UsageError(
32
+ "Authentication failed (401). Run 'expedait auth login'."
33
+ )
34
+ if resp.status_code == 403:
35
+ raise click.UsageError(
36
+ "Permission denied (403). Check your tenant access."
37
+ )
38
+ if resp.status_code == 404:
39
+ raise click.UsageError("Resource not found (404).")
40
+ if resp.status_code >= 400:
41
+ detail = ""
42
+ try:
43
+ detail = resp.json().get("detail", resp.text)
44
+ except Exception:
45
+ detail = resp.text
46
+ raise click.ClickException(f"API error {resp.status_code}: {detail}")
47
+
48
+ def _request(self, method: str, path: str, **kwargs: Any) -> Any:
49
+ """Make request, handle errors, return parsed JSON."""
50
+ resp = self._http.request(method, path, **kwargs)
51
+ self._check(resp)
52
+ if resp.status_code == 204 or not resp.content:
53
+ return None
54
+ return resp.json()
55
+
56
+ def _request_raw(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
57
+ """Make request, handle errors, return raw response."""
58
+ resp = self._http.request(method, path, **kwargs)
59
+ self._check(resp)
60
+ return resp
61
+
62
+ # -- auth -------------------------------------------------------------
63
+
64
+ @staticmethod
65
+ def login(api_url: str, email: str, password: str) -> dict[str, Any]:
66
+ """POST /api/v1/auth/login — returns token payload."""
67
+ resp = httpx.post(
68
+ f"{api_url}/api/v1/auth/login",
69
+ json={"email": email, "password": password},
70
+ timeout=15.0,
71
+ )
72
+ if resp.status_code == 401:
73
+ raise click.UsageError("Invalid email or password.")
74
+ if resp.status_code >= 400:
75
+ raise click.ClickException(f"Login failed ({resp.status_code}).")
76
+ return resp.json()
77
+
78
+ def get_me(self) -> dict[str, Any]:
79
+ return self._request("GET", "/api/v1/auth/me")
80
+
81
+ # -- projects ---------------------------------------------------------
82
+
83
+ def list_projects(self) -> list[dict[str, Any]]:
84
+ return self._request("GET", "/api/v1/projects")
85
+
86
+ def get_project(self, project_id: int) -> dict[str, Any]:
87
+ return self._request("GET", f"/api/v1/projects/{project_id}")
88
+
89
+ def get_workspace(self, project_id: int) -> dict[str, Any]:
90
+ return self._request("GET", f"/api/v1/projects/{project_id}/workspace")
91
+
92
+ def download_project(self, project_id: int) -> bytes:
93
+ resp = self._request_raw("GET", f"/api/v1/projects/{project_id}/download")
94
+ return resp.content
95
+
96
+ # -- deliverables -----------------------------------------------------
97
+
98
+ def list_deliverables(
99
+ self, project_id: int, skip: int = 0, limit: int = 100,
100
+ ) -> list[dict[str, Any]]:
101
+ return self._request(
102
+ "GET", "/api/v1/deliverables",
103
+ params={"project_id": project_id, "skip": skip, "limit": limit},
104
+ )
105
+
106
+ def get_deliverable(self, deliverable_id: int) -> dict[str, Any]:
107
+ return self._request("GET", f"/api/v1/deliverables/{deliverable_id}")
108
+
109
+ def get_deliverable_full(self, deliverable_id: int) -> dict[str, Any]:
110
+ """Full payload: dependencies, comments, versions, lock status."""
111
+ return self._request("GET", f"/api/v1/deliverables/{deliverable_id}/full")
112
+
113
+ def get_deliverable_type(self, type_id: int) -> dict[str, Any]:
114
+ return self._request("GET", f"/api/v1/deliverables/types/{type_id}")
115
+
116
+ def get_deliverable_sources(self, deliverable_id: int) -> list[dict[str, Any]]:
117
+ return self._request("GET", f"/api/v1/deliverables/{deliverable_id}/sources")
118
+
119
+ def download_deliverable(self, deliverable_id: int) -> bytes:
120
+ resp = self._request_raw(
121
+ "GET", f"/api/v1/deliverables/{deliverable_id}/download",
122
+ )
123
+ return resp.content
124
+
125
+ def get_objective_overview(self, deliverable_id: int) -> dict[str, Any]:
126
+ """Objective metadata + descendant tree. 400 if not an objective."""
127
+ resp = self._http.request(
128
+ "GET", f"/api/v1/deliverables/{deliverable_id}/objective-overview",
129
+ )
130
+ if resp.status_code == 400:
131
+ raise click.UsageError(
132
+ f"Deliverable {deliverable_id} is not an objective."
133
+ )
134
+ self._check(resp)
135
+ return resp.json()
136
+
137
+ def get_deliverable_context(self, deliverable_id: int) -> dict[str, Any]:
138
+ """Read-only LLM context snapshot for a deliverable."""
139
+ return self._request(
140
+ "GET", f"/api/v1/deliverables/{deliverable_id}/context-summary",
141
+ )
142
+
143
+ # -- review issues ----------------------------------------------------
144
+
145
+ def list_review_issues(
146
+ self, deliverable_id: int, state: str = "all",
147
+ ) -> list[dict[str, Any]]:
148
+ # Backend default (no state param) is open + muted == 'all'.
149
+ params = {} if state == "all" else {"state": state}
150
+ return self._request(
151
+ "GET", f"/api/v1/deliverables/{deliverable_id}/issues", params=params,
152
+ )
153
+
154
+ def mute_review_issue(
155
+ self, issue_id: int, muted: bool = True, note: str | None = None,
156
+ ) -> dict[str, Any]:
157
+ payload: dict[str, Any] = {"state": "muted" if muted else "open"}
158
+ if muted and note is not None:
159
+ payload["muted_note"] = note
160
+ return self._request(
161
+ "PATCH", f"/api/v1/deliverables/issues/{issue_id}", json=payload,
162
+ )
163
+
164
+ # -- comments ---------------------------------------------------------
165
+
166
+ def list_comments(self, deliverable_id: int) -> list[dict[str, Any]]:
167
+ return self._request("GET", f"/api/v1/deliverables/{deliverable_id}/comments")
168
+
169
+ def create_comment(self, deliverable_id: int, data: dict[str, Any]) -> dict[str, Any]:
170
+ return self._request(
171
+ "POST", f"/api/v1/deliverables/{deliverable_id}/comments", json=data,
172
+ )
173
+
174
+ def resolve_comment(self, deliverable_id: int, comment_id: int) -> dict[str, Any]:
175
+ return self._request(
176
+ "PUT",
177
+ f"/api/v1/deliverables/{deliverable_id}/comments/{comment_id}",
178
+ params={"is_resolved": "true"},
179
+ )
180
+
181
+ def delete_comment(self, deliverable_id: int, comment_id: int) -> Any:
182
+ return self._request(
183
+ "DELETE", f"/api/v1/deliverables/{deliverable_id}/comments/{comment_id}",
184
+ )