expedait-cli 0.2.2__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.
- expedait_cli-0.3.0/CHANGELOG.md +35 -0
- {expedait_cli-0.2.2 → expedait_cli-0.3.0}/PKG-INFO +64 -22
- expedait_cli-0.3.0/README.md +184 -0
- expedait_cli-0.3.0/expedait_cli/client.py +184 -0
- expedait_cli-0.3.0/expedait_cli/commands/comments.py +146 -0
- expedait_cli-0.3.0/expedait_cli/commands/context_cmd.py +40 -0
- expedait_cli-0.3.0/expedait_cli/commands/deliverables.py +245 -0
- expedait_cli-0.3.0/expedait_cli/commands/objectives.py +62 -0
- expedait_cli-0.3.0/expedait_cli/commands/review.py +81 -0
- {expedait_cli-0.2.2 → expedait_cli-0.3.0}/expedait_cli/main.py +10 -2
- {expedait_cli-0.2.2 → expedait_cli-0.3.0}/pyproject.toml +3 -1
- {expedait_cli-0.2.2 → expedait_cli-0.3.0}/tests/test_client.py +26 -2
- expedait_cli-0.3.0/tests/test_commands/test_comments.py +122 -0
- expedait_cli-0.3.0/tests/test_commands/test_context.py +32 -0
- expedait_cli-0.3.0/tests/test_commands/test_deliverables.py +141 -0
- expedait_cli-0.3.0/tests/test_commands/test_objectives.py +49 -0
- expedait_cli-0.3.0/tests/test_commands/test_review.py +62 -0
- {expedait_cli-0.2.2 → expedait_cli-0.3.0}/uv.lock +1 -1
- expedait_cli-0.2.2/README.md +0 -142
- expedait_cli-0.2.2/expedait_cli/client.py +0 -134
- expedait_cli-0.2.2/expedait_cli/commands/comments.py +0 -102
- expedait_cli-0.2.2/expedait_cli/commands/pages.py +0 -110
- expedait_cli-0.2.2/tests/test_commands/test_comments.py +0 -138
- expedait_cli-0.2.2/tests/test_commands/test_pages.py +0 -153
- {expedait_cli-0.2.2 → expedait_cli-0.3.0}/.github/workflows/ci.yml +0 -0
- {expedait_cli-0.2.2 → expedait_cli-0.3.0}/.github/workflows/publish.yml +0 -0
- {expedait_cli-0.2.2 → expedait_cli-0.3.0}/.gitignore +0 -0
- {expedait_cli-0.2.2 → expedait_cli-0.3.0}/CLAUDE.md +0 -0
- {expedait_cli-0.2.2 → expedait_cli-0.3.0}/LICENSE +0 -0
- {expedait_cli-0.2.2 → expedait_cli-0.3.0}/expedait_cli/__init__.py +0 -0
- {expedait_cli-0.2.2 → expedait_cli-0.3.0}/expedait_cli/auth.py +0 -0
- {expedait_cli-0.2.2 → expedait_cli-0.3.0}/expedait_cli/commands/__init__.py +0 -0
- {expedait_cli-0.2.2 → expedait_cli-0.3.0}/expedait_cli/commands/auth_cmd.py +0 -0
- {expedait_cli-0.2.2 → expedait_cli-0.3.0}/expedait_cli/commands/init_cmd.py +0 -0
- {expedait_cli-0.2.2 → expedait_cli-0.3.0}/expedait_cli/commands/projects.py +0 -0
- {expedait_cli-0.2.2 → expedait_cli-0.3.0}/expedait_cli/config.py +0 -0
- {expedait_cli-0.2.2 → expedait_cli-0.3.0}/expedait_cli/formatters.py +0 -0
- {expedait_cli-0.2.2 → expedait_cli-0.3.0}/expedait_cli/settings.py +0 -0
- {expedait_cli-0.2.2 → expedait_cli-0.3.0}/tests/__init__.py +0 -0
- {expedait_cli-0.2.2 → expedait_cli-0.3.0}/tests/conftest.py +0 -0
- {expedait_cli-0.2.2 → expedait_cli-0.3.0}/tests/test_auth.py +0 -0
- {expedait_cli-0.2.2 → expedait_cli-0.3.0}/tests/test_commands/__init__.py +0 -0
- {expedait_cli-0.2.2 → expedait_cli-0.3.0}/tests/test_commands/test_auth_cmd.py +0 -0
- {expedait_cli-0.2.2 → expedait_cli-0.3.0}/tests/test_commands/test_init_cmd.py +0 -0
- {expedait_cli-0.2.2 → expedait_cli-0.3.0}/tests/test_commands/test_projects.py +0 -0
- {expedait_cli-0.2.2 → expedait_cli-0.3.0}/tests/test_config.py +0 -0
- {expedait_cli-0.2.2 → expedait_cli-0.3.0}/tests/test_settings.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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: expedait-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: CLI for Expedait project management — download specs, post comments
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -17,6 +17,15 @@ Description-Content-Type: text/markdown
|
|
|
17
17
|
|
|
18
18
|
CLI for [Expedait](https://expedait.org) — lets AI coding agents download project specs and post comments via the Expedait API.
|
|
19
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
|
+
|
|
20
29
|
## Usage
|
|
21
30
|
|
|
22
31
|
### Run with `uvx` (recommended)
|
|
@@ -53,8 +62,8 @@ Once initialized, commands that need a project ID will resolve it automatically.
|
|
|
53
62
|
|
|
54
63
|
```bash
|
|
55
64
|
expedait projects download # downloads to .expedait/context/
|
|
56
|
-
expedait
|
|
57
|
-
expedait
|
|
65
|
+
expedait deliverables list # no --project-id needed
|
|
66
|
+
expedait deliverables download 42 # downloads to .expedait/context/
|
|
58
67
|
```
|
|
59
68
|
|
|
60
69
|
**Resolution order for tenant/project:** CLI flag > env var > `.expedait/settings.json` > `~/.expedait/config.json`.
|
|
@@ -92,37 +101,66 @@ expedait auth logout # Clear stored credentials
|
|
|
92
101
|
### Projects
|
|
93
102
|
|
|
94
103
|
```bash
|
|
95
|
-
expedait projects list
|
|
96
|
-
expedait projects get PROJECT_ID
|
|
97
|
-
expedait projects download PROJECT_ID
|
|
98
|
-
expedait projects download PROJECT_ID --
|
|
99
|
-
|
|
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
|
|
100
130
|
```
|
|
101
131
|
|
|
102
|
-
###
|
|
132
|
+
### Context
|
|
103
133
|
|
|
104
134
|
```bash
|
|
105
|
-
expedait
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
110
145
|
```
|
|
111
146
|
|
|
112
147
|
### Comments
|
|
113
148
|
|
|
114
149
|
```bash
|
|
115
|
-
expedait comments list
|
|
116
|
-
expedait comments create
|
|
150
|
+
expedait comments list DELIVERABLE_ID # List comments on a deliverable
|
|
151
|
+
expedait comments create DELIVERABLE_ID \ # Create a comment (offsets resolved automatically)
|
|
117
152
|
--text "Comment content" \
|
|
118
|
-
--selected-text "text from
|
|
119
|
-
--
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
expedait comments resolve PAGE_ID COMMENT_ID # Mark as resolved
|
|
123
|
-
expedait comments delete PAGE_ID COMMENT_ID # Delete a comment
|
|
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
|
|
124
157
|
```
|
|
125
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
|
+
|
|
126
164
|
### Global Options
|
|
127
165
|
|
|
128
166
|
```bash
|
|
@@ -135,6 +173,10 @@ expedait --version # Show version
|
|
|
135
173
|
|
|
136
174
|
Output format defaults to `text` when connected to a terminal, `json` when piped.
|
|
137
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
|
+
|
|
138
180
|
## Agent Skills
|
|
139
181
|
|
|
140
182
|
For step-by-step guides on using the CLI from AI coding agents, see [expedait-skills](https://github.com/Expedait/expedait-skills).
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Expedait CLI
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/expedait-cli/)
|
|
4
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
5
|
+
[](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)
|
|
@@ -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
|
+
)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Comment commands: list, create, resolve, delete."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from ..auth import resolve_api_url, resolve_tenant_id, resolve_token
|
|
8
|
+
from ..client import ExpedaitClient
|
|
9
|
+
from ..formatters import output
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _make_client(ctx: click.Context) -> ExpedaitClient:
|
|
13
|
+
token = resolve_token()
|
|
14
|
+
api_url = resolve_api_url(ctx.obj.get("api_url"))
|
|
15
|
+
tenant_id = resolve_tenant_id(ctx.obj.get("tenant_id"))
|
|
16
|
+
return ExpedaitClient(api_url, token, tenant_id)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _resolve_offsets(
|
|
20
|
+
client: ExpedaitClient,
|
|
21
|
+
deliverable_id: int,
|
|
22
|
+
selected_text: str,
|
|
23
|
+
start_offset: int | None,
|
|
24
|
+
end_offset: int | None,
|
|
25
|
+
) -> tuple[int, int]:
|
|
26
|
+
"""Resolve anchor offsets from the deliverable content.
|
|
27
|
+
|
|
28
|
+
The backend requires start/end offsets, but callers should only have to
|
|
29
|
+
supply the selected text. If both offsets are given we trust them;
|
|
30
|
+
otherwise we locate the selected text in the current content.
|
|
31
|
+
"""
|
|
32
|
+
if start_offset is not None and end_offset is not None:
|
|
33
|
+
return start_offset, end_offset
|
|
34
|
+
|
|
35
|
+
deliverable = client.get_deliverable(deliverable_id)
|
|
36
|
+
content = deliverable.get("content") or ""
|
|
37
|
+
idx = content.find(selected_text)
|
|
38
|
+
if idx < 0:
|
|
39
|
+
raise click.UsageError(
|
|
40
|
+
"Could not find the selected text in deliverable "
|
|
41
|
+
f"{deliverable_id}. Pass --start-offset/--end-offset explicitly."
|
|
42
|
+
)
|
|
43
|
+
if content.find(selected_text, idx + 1) >= 0:
|
|
44
|
+
click.echo(
|
|
45
|
+
"Warning: selected text appears multiple times; anchoring to the "
|
|
46
|
+
"first occurrence. Pass --start-offset/--end-offset to disambiguate.",
|
|
47
|
+
err=True,
|
|
48
|
+
)
|
|
49
|
+
return idx, idx + len(selected_text)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@click.group()
|
|
53
|
+
def comments() -> None:
|
|
54
|
+
"""Manage deliverable comments."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@comments.command("list")
|
|
58
|
+
@click.argument("deliverable_id", type=int)
|
|
59
|
+
@click.pass_context
|
|
60
|
+
def list_comments(ctx: click.Context, deliverable_id: int) -> None:
|
|
61
|
+
"""List comments on a deliverable."""
|
|
62
|
+
client = _make_client(ctx)
|
|
63
|
+
try:
|
|
64
|
+
data = client.list_comments(deliverable_id)
|
|
65
|
+
finally:
|
|
66
|
+
client.close()
|
|
67
|
+
output(data, ctx.obj.get("fmt"))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@comments.command("create")
|
|
71
|
+
@click.argument("deliverable_id", type=int)
|
|
72
|
+
@click.option("--text", required=True, help="Comment content.")
|
|
73
|
+
@click.option("--selected-text", required=True, help="The text being commented on.")
|
|
74
|
+
@click.option("--start-offset", type=int, default=None, help="Start char offset (resolved from content if omitted).")
|
|
75
|
+
@click.option("--end-offset", type=int, default=None, help="End char offset (resolved from content if omitted).")
|
|
76
|
+
@click.option("--source-deliverable-id", type=int, default=None, help="Agent's source deliverable ID.")
|
|
77
|
+
@click.option("--parent-comment-id", type=int, default=None, help="Reply to comment ID.")
|
|
78
|
+
@click.option("--agent-run-id", type=int, default=None, help="Link comment to a build run.")
|
|
79
|
+
@click.pass_context
|
|
80
|
+
def create_comment(
|
|
81
|
+
ctx: click.Context,
|
|
82
|
+
deliverable_id: int,
|
|
83
|
+
text: str,
|
|
84
|
+
selected_text: str,
|
|
85
|
+
start_offset: int | None,
|
|
86
|
+
end_offset: int | None,
|
|
87
|
+
source_deliverable_id: int | None,
|
|
88
|
+
parent_comment_id: int | None,
|
|
89
|
+
agent_run_id: int | None,
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Create a comment on a deliverable.
|
|
92
|
+
|
|
93
|
+
Only --text and --selected-text are required; anchor offsets are resolved
|
|
94
|
+
from the deliverable content automatically.
|
|
95
|
+
"""
|
|
96
|
+
client = _make_client(ctx)
|
|
97
|
+
try:
|
|
98
|
+
start, end = _resolve_offsets(
|
|
99
|
+
client, deliverable_id, selected_text, start_offset, end_offset,
|
|
100
|
+
)
|
|
101
|
+
payload: dict = {
|
|
102
|
+
"comment_text": text,
|
|
103
|
+
"selected_text": selected_text,
|
|
104
|
+
"start_offset": start,
|
|
105
|
+
"end_offset": end,
|
|
106
|
+
"is_agent_comment": True,
|
|
107
|
+
}
|
|
108
|
+
if source_deliverable_id is not None:
|
|
109
|
+
payload["source_deliverable_id"] = source_deliverable_id
|
|
110
|
+
if parent_comment_id is not None:
|
|
111
|
+
payload["parent_comment_id"] = parent_comment_id
|
|
112
|
+
if agent_run_id is not None:
|
|
113
|
+
payload["agent_run_id"] = agent_run_id
|
|
114
|
+
|
|
115
|
+
data = client.create_comment(deliverable_id, payload)
|
|
116
|
+
finally:
|
|
117
|
+
client.close()
|
|
118
|
+
output(data, ctx.obj.get("fmt"))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@comments.command("resolve")
|
|
122
|
+
@click.argument("deliverable_id", type=int)
|
|
123
|
+
@click.argument("comment_id", type=int)
|
|
124
|
+
@click.pass_context
|
|
125
|
+
def resolve_comment(ctx: click.Context, deliverable_id: int, comment_id: int) -> None:
|
|
126
|
+
"""Mark a comment as resolved."""
|
|
127
|
+
client = _make_client(ctx)
|
|
128
|
+
try:
|
|
129
|
+
data = client.resolve_comment(deliverable_id, comment_id)
|
|
130
|
+
finally:
|
|
131
|
+
client.close()
|
|
132
|
+
output(data, ctx.obj.get("fmt"))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@comments.command("delete")
|
|
136
|
+
@click.argument("deliverable_id", type=int)
|
|
137
|
+
@click.argument("comment_id", type=int)
|
|
138
|
+
@click.pass_context
|
|
139
|
+
def delete_comment(ctx: click.Context, deliverable_id: int, comment_id: int) -> None:
|
|
140
|
+
"""Delete a comment."""
|
|
141
|
+
client = _make_client(ctx)
|
|
142
|
+
try:
|
|
143
|
+
client.delete_comment(deliverable_id, comment_id)
|
|
144
|
+
finally:
|
|
145
|
+
client.close()
|
|
146
|
+
click.echo("Comment deleted.")
|