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.
- expedait_cli-0.3.0/CHANGELOG.md +35 -0
- expedait_cli-0.3.0/PKG-INFO +195 -0
- expedait_cli-0.3.0/README.md +184 -0
- {expedait_cli-0.2.1 → expedait_cli-0.3.0}/expedait_cli/auth.py +30 -2
- 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/init_cmd.py +77 -0
- expedait_cli-0.3.0/expedait_cli/commands/objectives.py +62 -0
- {expedait_cli-0.2.1 → expedait_cli-0.3.0}/expedait_cli/commands/projects.py +11 -5
- expedait_cli-0.3.0/expedait_cli/commands/review.py +81 -0
- {expedait_cli-0.2.1 → expedait_cli-0.3.0}/expedait_cli/main.py +12 -2
- expedait_cli-0.3.0/expedait_cli/settings.py +31 -0
- {expedait_cli-0.2.1 → expedait_cli-0.3.0}/pyproject.toml +3 -1
- {expedait_cli-0.2.1 → expedait_cli-0.3.0}/tests/test_auth.py +30 -3
- {expedait_cli-0.2.1 → 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_init_cmd.py +161 -0
- expedait_cli-0.3.0/tests/test_commands/test_objectives.py +49 -0
- {expedait_cli-0.2.1 → expedait_cli-0.3.0}/tests/test_commands/test_projects.py +32 -0
- expedait_cli-0.3.0/tests/test_commands/test_review.py +62 -0
- expedait_cli-0.3.0/tests/test_settings.py +33 -0
- {expedait_cli-0.2.1 → expedait_cli-0.3.0}/uv.lock +1 -1
- expedait_cli-0.2.1/PKG-INFO +0 -153
- expedait_cli-0.2.1/README.md +0 -142
- expedait_cli-0.2.1/expedait_cli/client.py +0 -130
- expedait_cli-0.2.1/expedait_cli/commands/comments.py +0 -102
- expedait_cli-0.2.1/expedait_cli/commands/pages.py +0 -104
- expedait_cli-0.2.1/tests/test_commands/test_comments.py +0 -138
- expedait_cli-0.2.1/tests/test_commands/test_pages.py +0 -121
- {expedait_cli-0.2.1 → expedait_cli-0.3.0}/.github/workflows/ci.yml +0 -0
- {expedait_cli-0.2.1 → expedait_cli-0.3.0}/.github/workflows/publish.yml +0 -0
- {expedait_cli-0.2.1 → expedait_cli-0.3.0}/.gitignore +0 -0
- {expedait_cli-0.2.1 → expedait_cli-0.3.0}/CLAUDE.md +0 -0
- {expedait_cli-0.2.1 → expedait_cli-0.3.0}/LICENSE +0 -0
- {expedait_cli-0.2.1 → expedait_cli-0.3.0}/expedait_cli/__init__.py +0 -0
- {expedait_cli-0.2.1 → expedait_cli-0.3.0}/expedait_cli/commands/__init__.py +0 -0
- {expedait_cli-0.2.1 → expedait_cli-0.3.0}/expedait_cli/commands/auth_cmd.py +0 -0
- {expedait_cli-0.2.1 → expedait_cli-0.3.0}/expedait_cli/config.py +0 -0
- {expedait_cli-0.2.1 → expedait_cli-0.3.0}/expedait_cli/formatters.py +0 -0
- {expedait_cli-0.2.1 → expedait_cli-0.3.0}/tests/__init__.py +0 -0
- {expedait_cli-0.2.1 → expedait_cli-0.3.0}/tests/conftest.py +0 -0
- {expedait_cli-0.2.1 → expedait_cli-0.3.0}/tests/test_commands/__init__.py +0 -0
- {expedait_cli-0.2.1 → expedait_cli-0.3.0}/tests/test_commands/test_auth_cmd.py +0 -0
- {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
|
+
[](https://pypi.org/project/expedait-cli/)
|
|
15
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
16
|
+
[](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
|
+
[](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)
|
|
@@ -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(
|
|
45
|
-
|
|
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
|
+
)
|