projectmemory-cli 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- projectmemory_cli-0.1.0/PKG-INFO +239 -0
- projectmemory_cli-0.1.0/README.md +216 -0
- projectmemory_cli-0.1.0/projectmemory_cli/__init__.py +3 -0
- projectmemory_cli-0.1.0/projectmemory_cli/api/__init__.py +1 -0
- projectmemory_cli-0.1.0/projectmemory_cli/api/auth.py +94 -0
- projectmemory_cli-0.1.0/projectmemory_cli/api/client.py +135 -0
- projectmemory_cli-0.1.0/projectmemory_cli/api/errors.py +124 -0
- projectmemory_cli-0.1.0/projectmemory_cli/api/sessions.py +25 -0
- projectmemory_cli-0.1.0/projectmemory_cli/app.py +80 -0
- projectmemory_cli-0.1.0/projectmemory_cli/commands/__init__.py +1 -0
- projectmemory_cli-0.1.0/projectmemory_cli/commands/auth.py +163 -0
- projectmemory_cli-0.1.0/projectmemory_cli/commands/commits.py +147 -0
- projectmemory_cli-0.1.0/projectmemory_cli/commands/config.py +56 -0
- projectmemory_cli-0.1.0/projectmemory_cli/commands/doctor.py +375 -0
- projectmemory_cli-0.1.0/projectmemory_cli/commands/issues.py +234 -0
- projectmemory_cli-0.1.0/projectmemory_cli/commands/memory.py +235 -0
- projectmemory_cli-0.1.0/projectmemory_cli/commands/next_action.py +282 -0
- projectmemory_cli-0.1.0/projectmemory_cli/commands/notes.py +231 -0
- projectmemory_cli-0.1.0/projectmemory_cli/commands/projects.py +325 -0
- projectmemory_cli-0.1.0/projectmemory_cli/commands/resume.py +240 -0
- projectmemory_cli-0.1.0/projectmemory_cli/commands/sessions.py +277 -0
- projectmemory_cli-0.1.0/projectmemory_cli/commands/tasks.py +435 -0
- projectmemory_cli-0.1.0/projectmemory_cli/config.py +101 -0
- projectmemory_cli-0.1.0/projectmemory_cli/credentials.py +98 -0
- projectmemory_cli-0.1.0/projectmemory_cli/git.py +124 -0
- projectmemory_cli-0.1.0/projectmemory_cli/main.py +4 -0
- projectmemory_cli-0.1.0/projectmemory_cli/models/__init__.py +1 -0
- projectmemory_cli-0.1.0/projectmemory_cli/models/api.py +226 -0
- projectmemory_cli-0.1.0/projectmemory_cli/output.py +227 -0
- projectmemory_cli-0.1.0/projectmemory_cli/project_context.py +244 -0
- projectmemory_cli-0.1.0/pyproject.toml +54 -0
- projectmemory_cli-0.1.0/tests/conftest.py +20 -0
- projectmemory_cli-0.1.0/tests/test_auth.py +119 -0
- projectmemory_cli-0.1.0/tests/test_doctor.py +14 -0
- projectmemory_cli-0.1.0/tests/test_errors.py +75 -0
- projectmemory_cli-0.1.0/tests/test_git.py +32 -0
- projectmemory_cli-0.1.0/tests/test_guard.py +85 -0
- projectmemory_cli-0.1.0/tests/test_output.py +31 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: projectmemory-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Project Memory CLI — developer context in your terminal
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: httpx>=0.27
|
|
8
|
+
Requires-Dist: keyring>=25
|
|
9
|
+
Requires-Dist: platformdirs>=4
|
|
10
|
+
Requires-Dist: pydantic>=2
|
|
11
|
+
Requires-Dist: questionary>=2
|
|
12
|
+
Requires-Dist: rich>=13
|
|
13
|
+
Requires-Dist: tomli-w>=1
|
|
14
|
+
Requires-Dist: typer[all]>=0.12
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
17
|
+
Requires-Dist: pytest-cov>=5; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest-mock>=3; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
21
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# Project Memory CLI
|
|
25
|
+
|
|
26
|
+
Developer context in your terminal — a Python CLI client for [projectmemory.app](https://projectmemory.app).
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install projectmemory-cli
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or from source:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
git clone https://github.com/your-org/projectmemory-cli
|
|
38
|
+
cd projectmemory-cli
|
|
39
|
+
pip install -e ".[dev]"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Both `pm` and `projectmemory` are registered as executable names.
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Authenticate (opens browser)
|
|
48
|
+
pm login
|
|
49
|
+
|
|
50
|
+
# Create a project and start its first session
|
|
51
|
+
pm project create
|
|
52
|
+
|
|
53
|
+
# Or resume an existing project/session
|
|
54
|
+
pm resume "My App"
|
|
55
|
+
|
|
56
|
+
# See your current context
|
|
57
|
+
pm memory show
|
|
58
|
+
|
|
59
|
+
# Work with tasks
|
|
60
|
+
pm task list
|
|
61
|
+
pm task add "Implement login"
|
|
62
|
+
pm task done 42 # completes task, then optionally links a commit
|
|
63
|
+
pm done # interactive task completion shortcut
|
|
64
|
+
|
|
65
|
+
# Capture notes
|
|
66
|
+
pm capture "Don't forget to update the Stripe webhook URL"
|
|
67
|
+
|
|
68
|
+
# Report an issue
|
|
69
|
+
pm issue add "Login fails on mobile Safari"
|
|
70
|
+
|
|
71
|
+
# End your session with the shared backend review
|
|
72
|
+
pm session end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Commands
|
|
76
|
+
|
|
77
|
+
### Authentication
|
|
78
|
+
| Command | Description |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `pm login` | Authenticate via browser |
|
|
81
|
+
| `pm logout` | Revoke token |
|
|
82
|
+
| `pm whoami` | Show current user |
|
|
83
|
+
| `pm auth status` | Check auth status |
|
|
84
|
+
| `pm doctor` | Diagnose CLI config, auth, backend, project, session, and Git state |
|
|
85
|
+
|
|
86
|
+
### Projects
|
|
87
|
+
| Command | Description |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `pm project list` | List all projects |
|
|
90
|
+
| `pm project create` | Create a project (interactive) |
|
|
91
|
+
| `pm project current` | Show current project + session |
|
|
92
|
+
| `pm project link` | Link this directory to a project |
|
|
93
|
+
| `pm project unlink` | Remove directory mapping |
|
|
94
|
+
| `pm web` | Open project in browser |
|
|
95
|
+
|
|
96
|
+
### Work Sessions
|
|
97
|
+
| Command | Description |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `pm resume [project]` | Start or resume a session |
|
|
100
|
+
| `pm session status` | Show all active sessions |
|
|
101
|
+
| `pm session end` | End the current session |
|
|
102
|
+
|
|
103
|
+
### Project Memory
|
|
104
|
+
| Command | Description |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `pm memory show` | Display all memory fields |
|
|
107
|
+
| `pm memory status set [text]` | Update Current Status |
|
|
108
|
+
| `pm memory context set [text]` | Update Important Context |
|
|
109
|
+
| `pm memory update` | Interactive memory update, including open issues |
|
|
110
|
+
|
|
111
|
+
### Next Action
|
|
112
|
+
| Command | Description |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `pm next` | Show current Next Action |
|
|
115
|
+
| `pm next set [text]` | Set Next Action (interactive menu) |
|
|
116
|
+
| `pm next task <id>` | Link a task as Next Action |
|
|
117
|
+
| `pm next issue <id>` | Link an issue as Next Action |
|
|
118
|
+
| `pm next clear` | Clear Next Action |
|
|
119
|
+
|
|
120
|
+
### Tasks
|
|
121
|
+
| Command | Description |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `pm task list` | List tasks; supports `--open`, `--completed`, `--snoozed`, `--all` |
|
|
124
|
+
| `pm task add [title]` | Create a task |
|
|
125
|
+
| `pm task done <id>` | Complete task + optional commit link |
|
|
126
|
+
| `pm done` | Interactive task completion shortcut |
|
|
127
|
+
| `pm task snooze <id>` | Snooze task |
|
|
128
|
+
| `pm task reopen <id>` | Reopen task |
|
|
129
|
+
| `pm task edit <id>` | Edit task |
|
|
130
|
+
| `pm task delete <id>` | Delete task |
|
|
131
|
+
|
|
132
|
+
### Notes
|
|
133
|
+
| Command | Description |
|
|
134
|
+
|---|---|
|
|
135
|
+
| `pm note list` | List notes |
|
|
136
|
+
| `pm note add [content]` | Create a note; supports `--title`, `--editor`, `--stdin` |
|
|
137
|
+
| `pm note search <query>` | Search notes |
|
|
138
|
+
| `pm capture [text]` | Quick note shortcut |
|
|
139
|
+
| `pm note edit <id>` | Edit a note |
|
|
140
|
+
| `pm note delete <id>` | Delete a note |
|
|
141
|
+
|
|
142
|
+
### Issues
|
|
143
|
+
| Command | Description |
|
|
144
|
+
|---|---|
|
|
145
|
+
| `pm issue list` | List issues; supports `--open`, `--resolved`, `--all` |
|
|
146
|
+
| `pm issue add [description]` | Report an issue |
|
|
147
|
+
| `pm issue resolve <id>` | Mark issue as resolved |
|
|
148
|
+
| `pm issue reopen <id>` | Reopen issue |
|
|
149
|
+
| `pm issue edit <id>` | Edit issue |
|
|
150
|
+
| `pm issue delete <id>` | Delete issue |
|
|
151
|
+
|
|
152
|
+
### Commits
|
|
153
|
+
| Command | Description |
|
|
154
|
+
|---|---|
|
|
155
|
+
| `pm commit link` | Interactive recovery flow for linking a commit to a completed task |
|
|
156
|
+
| `pm commit link HEAD --task 24` | Directly link a commit/ref to a completed task |
|
|
157
|
+
|
|
158
|
+
## Configuration
|
|
159
|
+
|
|
160
|
+
Configuration is stored at `~/.config/projectmemory/config.toml`.
|
|
161
|
+
|
|
162
|
+
The default `api_url` is the Project Memory site root, for example `https://projectmemory.app`. The CLI adds API paths such as `/api/cli/...` internally.
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
pm config show # show all settings
|
|
166
|
+
pm config set api_url https://your-instance.example.com
|
|
167
|
+
pm config reset # restore defaults
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Environment variables
|
|
171
|
+
|
|
172
|
+
| Variable | Description |
|
|
173
|
+
|---|---|
|
|
174
|
+
| `PROJECTMEMORY_API_URL` | API base URL |
|
|
175
|
+
| `PROJECTMEMORY_TOKEN` | Token override (skip keyring) |
|
|
176
|
+
| `PROJECTMEMORY_PROJECT` | Current project ID override |
|
|
177
|
+
| `PROJECTMEMORY_NO_COLOR` | Disable color output |
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
## Diagnostics
|
|
181
|
+
|
|
182
|
+
Run a read-only diagnostic report when troubleshooting local configuration or support issues:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
pm doctor
|
|
186
|
+
pm doctor --json
|
|
187
|
+
pm doctor --verbose
|
|
188
|
+
pm doctor --no-network
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`pm doctor` never prints authentication tokens, refresh tokens, passwords, private keys, or OAuth secrets. It exits with `0` when healthy, `1` when warnings are present, and `2` when critical issues prevent normal CLI use.
|
|
192
|
+
|
|
193
|
+
## Directory Linking
|
|
194
|
+
|
|
195
|
+
Link the current git repository to a project so `pm` knows which project to use automatically:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
cd ~/code/my-app
|
|
199
|
+
pm project link "My App"
|
|
200
|
+
|
|
201
|
+
# Now pm commands in this directory use "My App" automatically
|
|
202
|
+
pm task list # no --project needed
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## How Sessions Work
|
|
206
|
+
|
|
207
|
+
The CLI shares the session model with the web app. Sessions must be active for write operations (creating tasks, notes, issues, updating memory).
|
|
208
|
+
|
|
209
|
+
- `pm resume` starts or resumes the backend session for the selected project
|
|
210
|
+
- `pm session end` shows the shared backend Session Review and finalizes that exact session
|
|
211
|
+
- If the web app ends your session, the CLI will show a clear error and ask you to `pm resume` again
|
|
212
|
+
- A user can have active sessions in multiple projects, but the CLI has one current project
|
|
213
|
+
- Switching projects can end the current project session, keep it active, or cancel
|
|
214
|
+
|
|
215
|
+
## Development
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
# Install with dev dependencies
|
|
219
|
+
pip install -e ".[dev]"
|
|
220
|
+
|
|
221
|
+
# Run tests
|
|
222
|
+
pytest
|
|
223
|
+
|
|
224
|
+
# Run with coverage
|
|
225
|
+
pytest --cov=projectmemory_cli --cov-report=term-missing
|
|
226
|
+
|
|
227
|
+
# Lint
|
|
228
|
+
ruff check projectmemory_cli/
|
|
229
|
+
|
|
230
|
+
# Type check
|
|
231
|
+
mypy projectmemory_cli/
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Security
|
|
235
|
+
|
|
236
|
+
- Tokens are stored in the OS keyring (or `~/.config/projectmemory/credentials.json` with `chmod 600` as fallback)
|
|
237
|
+
- Passwords are never stored
|
|
238
|
+
- The `PROJECTMEMORY_TOKEN` environment variable overrides stored credentials (read-only)
|
|
239
|
+
- Tokens can be revoked remotely via `pm logout` or from the web app at projectmemory.app/settings
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# Project Memory CLI
|
|
2
|
+
|
|
3
|
+
Developer context in your terminal — a Python CLI client for [projectmemory.app](https://projectmemory.app).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install projectmemory-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or from source:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
git clone https://github.com/your-org/projectmemory-cli
|
|
15
|
+
cd projectmemory-cli
|
|
16
|
+
pip install -e ".[dev]"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Both `pm` and `projectmemory` are registered as executable names.
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Authenticate (opens browser)
|
|
25
|
+
pm login
|
|
26
|
+
|
|
27
|
+
# Create a project and start its first session
|
|
28
|
+
pm project create
|
|
29
|
+
|
|
30
|
+
# Or resume an existing project/session
|
|
31
|
+
pm resume "My App"
|
|
32
|
+
|
|
33
|
+
# See your current context
|
|
34
|
+
pm memory show
|
|
35
|
+
|
|
36
|
+
# Work with tasks
|
|
37
|
+
pm task list
|
|
38
|
+
pm task add "Implement login"
|
|
39
|
+
pm task done 42 # completes task, then optionally links a commit
|
|
40
|
+
pm done # interactive task completion shortcut
|
|
41
|
+
|
|
42
|
+
# Capture notes
|
|
43
|
+
pm capture "Don't forget to update the Stripe webhook URL"
|
|
44
|
+
|
|
45
|
+
# Report an issue
|
|
46
|
+
pm issue add "Login fails on mobile Safari"
|
|
47
|
+
|
|
48
|
+
# End your session with the shared backend review
|
|
49
|
+
pm session end
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Commands
|
|
53
|
+
|
|
54
|
+
### Authentication
|
|
55
|
+
| Command | Description |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `pm login` | Authenticate via browser |
|
|
58
|
+
| `pm logout` | Revoke token |
|
|
59
|
+
| `pm whoami` | Show current user |
|
|
60
|
+
| `pm auth status` | Check auth status |
|
|
61
|
+
| `pm doctor` | Diagnose CLI config, auth, backend, project, session, and Git state |
|
|
62
|
+
|
|
63
|
+
### Projects
|
|
64
|
+
| Command | Description |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `pm project list` | List all projects |
|
|
67
|
+
| `pm project create` | Create a project (interactive) |
|
|
68
|
+
| `pm project current` | Show current project + session |
|
|
69
|
+
| `pm project link` | Link this directory to a project |
|
|
70
|
+
| `pm project unlink` | Remove directory mapping |
|
|
71
|
+
| `pm web` | Open project in browser |
|
|
72
|
+
|
|
73
|
+
### Work Sessions
|
|
74
|
+
| Command | Description |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `pm resume [project]` | Start or resume a session |
|
|
77
|
+
| `pm session status` | Show all active sessions |
|
|
78
|
+
| `pm session end` | End the current session |
|
|
79
|
+
|
|
80
|
+
### Project Memory
|
|
81
|
+
| Command | Description |
|
|
82
|
+
|---|---|
|
|
83
|
+
| `pm memory show` | Display all memory fields |
|
|
84
|
+
| `pm memory status set [text]` | Update Current Status |
|
|
85
|
+
| `pm memory context set [text]` | Update Important Context |
|
|
86
|
+
| `pm memory update` | Interactive memory update, including open issues |
|
|
87
|
+
|
|
88
|
+
### Next Action
|
|
89
|
+
| Command | Description |
|
|
90
|
+
|---|---|
|
|
91
|
+
| `pm next` | Show current Next Action |
|
|
92
|
+
| `pm next set [text]` | Set Next Action (interactive menu) |
|
|
93
|
+
| `pm next task <id>` | Link a task as Next Action |
|
|
94
|
+
| `pm next issue <id>` | Link an issue as Next Action |
|
|
95
|
+
| `pm next clear` | Clear Next Action |
|
|
96
|
+
|
|
97
|
+
### Tasks
|
|
98
|
+
| Command | Description |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `pm task list` | List tasks; supports `--open`, `--completed`, `--snoozed`, `--all` |
|
|
101
|
+
| `pm task add [title]` | Create a task |
|
|
102
|
+
| `pm task done <id>` | Complete task + optional commit link |
|
|
103
|
+
| `pm done` | Interactive task completion shortcut |
|
|
104
|
+
| `pm task snooze <id>` | Snooze task |
|
|
105
|
+
| `pm task reopen <id>` | Reopen task |
|
|
106
|
+
| `pm task edit <id>` | Edit task |
|
|
107
|
+
| `pm task delete <id>` | Delete task |
|
|
108
|
+
|
|
109
|
+
### Notes
|
|
110
|
+
| Command | Description |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `pm note list` | List notes |
|
|
113
|
+
| `pm note add [content]` | Create a note; supports `--title`, `--editor`, `--stdin` |
|
|
114
|
+
| `pm note search <query>` | Search notes |
|
|
115
|
+
| `pm capture [text]` | Quick note shortcut |
|
|
116
|
+
| `pm note edit <id>` | Edit a note |
|
|
117
|
+
| `pm note delete <id>` | Delete a note |
|
|
118
|
+
|
|
119
|
+
### Issues
|
|
120
|
+
| Command | Description |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `pm issue list` | List issues; supports `--open`, `--resolved`, `--all` |
|
|
123
|
+
| `pm issue add [description]` | Report an issue |
|
|
124
|
+
| `pm issue resolve <id>` | Mark issue as resolved |
|
|
125
|
+
| `pm issue reopen <id>` | Reopen issue |
|
|
126
|
+
| `pm issue edit <id>` | Edit issue |
|
|
127
|
+
| `pm issue delete <id>` | Delete issue |
|
|
128
|
+
|
|
129
|
+
### Commits
|
|
130
|
+
| Command | Description |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `pm commit link` | Interactive recovery flow for linking a commit to a completed task |
|
|
133
|
+
| `pm commit link HEAD --task 24` | Directly link a commit/ref to a completed task |
|
|
134
|
+
|
|
135
|
+
## Configuration
|
|
136
|
+
|
|
137
|
+
Configuration is stored at `~/.config/projectmemory/config.toml`.
|
|
138
|
+
|
|
139
|
+
The default `api_url` is the Project Memory site root, for example `https://projectmemory.app`. The CLI adds API paths such as `/api/cli/...` internally.
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
pm config show # show all settings
|
|
143
|
+
pm config set api_url https://your-instance.example.com
|
|
144
|
+
pm config reset # restore defaults
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Environment variables
|
|
148
|
+
|
|
149
|
+
| Variable | Description |
|
|
150
|
+
|---|---|
|
|
151
|
+
| `PROJECTMEMORY_API_URL` | API base URL |
|
|
152
|
+
| `PROJECTMEMORY_TOKEN` | Token override (skip keyring) |
|
|
153
|
+
| `PROJECTMEMORY_PROJECT` | Current project ID override |
|
|
154
|
+
| `PROJECTMEMORY_NO_COLOR` | Disable color output |
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
## Diagnostics
|
|
158
|
+
|
|
159
|
+
Run a read-only diagnostic report when troubleshooting local configuration or support issues:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
pm doctor
|
|
163
|
+
pm doctor --json
|
|
164
|
+
pm doctor --verbose
|
|
165
|
+
pm doctor --no-network
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`pm doctor` never prints authentication tokens, refresh tokens, passwords, private keys, or OAuth secrets. It exits with `0` when healthy, `1` when warnings are present, and `2` when critical issues prevent normal CLI use.
|
|
169
|
+
|
|
170
|
+
## Directory Linking
|
|
171
|
+
|
|
172
|
+
Link the current git repository to a project so `pm` knows which project to use automatically:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
cd ~/code/my-app
|
|
176
|
+
pm project link "My App"
|
|
177
|
+
|
|
178
|
+
# Now pm commands in this directory use "My App" automatically
|
|
179
|
+
pm task list # no --project needed
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## How Sessions Work
|
|
183
|
+
|
|
184
|
+
The CLI shares the session model with the web app. Sessions must be active for write operations (creating tasks, notes, issues, updating memory).
|
|
185
|
+
|
|
186
|
+
- `pm resume` starts or resumes the backend session for the selected project
|
|
187
|
+
- `pm session end` shows the shared backend Session Review and finalizes that exact session
|
|
188
|
+
- If the web app ends your session, the CLI will show a clear error and ask you to `pm resume` again
|
|
189
|
+
- A user can have active sessions in multiple projects, but the CLI has one current project
|
|
190
|
+
- Switching projects can end the current project session, keep it active, or cancel
|
|
191
|
+
|
|
192
|
+
## Development
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# Install with dev dependencies
|
|
196
|
+
pip install -e ".[dev]"
|
|
197
|
+
|
|
198
|
+
# Run tests
|
|
199
|
+
pytest
|
|
200
|
+
|
|
201
|
+
# Run with coverage
|
|
202
|
+
pytest --cov=projectmemory_cli --cov-report=term-missing
|
|
203
|
+
|
|
204
|
+
# Lint
|
|
205
|
+
ruff check projectmemory_cli/
|
|
206
|
+
|
|
207
|
+
# Type check
|
|
208
|
+
mypy projectmemory_cli/
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Security
|
|
212
|
+
|
|
213
|
+
- Tokens are stored in the OS keyring (or `~/.config/projectmemory/credentials.json` with `chmod 600` as fallback)
|
|
214
|
+
- Passwords are never stored
|
|
215
|
+
- The `PROJECTMEMORY_TOKEN` environment variable overrides stored credentials (read-only)
|
|
216
|
+
- Tokens can be revoked remotely via `pm logout` or from the web app at projectmemory.app/settings
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""API package."""
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Device authorization flow API calls."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from projectmemory_cli.api.client import APIClient
|
|
8
|
+
from projectmemory_cli.api.errors import APIError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class DeviceAuthStart:
|
|
13
|
+
device_code: str
|
|
14
|
+
user_code: str
|
|
15
|
+
verification_uri: str
|
|
16
|
+
verification_uri_complete: str
|
|
17
|
+
expires_in: int
|
|
18
|
+
interval: int
|
|
19
|
+
authorization_id: int
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class DeviceAuthResult:
|
|
24
|
+
access_token: str
|
|
25
|
+
token_type: str
|
|
26
|
+
token_id: int
|
|
27
|
+
user: dict
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def start_device_auth(client: APIClient, device_name: str = "Project Memory CLI") -> DeviceAuthStart:
|
|
31
|
+
data = client.post("/api/cli/auth/device/", json={"device_name": device_name})
|
|
32
|
+
return DeviceAuthStart(
|
|
33
|
+
device_code=data["device_code"],
|
|
34
|
+
user_code=data["user_code"],
|
|
35
|
+
verification_uri=data["verification_uri"],
|
|
36
|
+
verification_uri_complete=data["verification_uri_complete"],
|
|
37
|
+
expires_in=data["expires_in"],
|
|
38
|
+
interval=data["interval"],
|
|
39
|
+
authorization_id=data["authorization_id"],
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def poll_device_auth(
|
|
44
|
+
client: APIClient,
|
|
45
|
+
device_code: str,
|
|
46
|
+
*,
|
|
47
|
+
interval: int = 5,
|
|
48
|
+
timeout: int = 900,
|
|
49
|
+
on_pending: object = None,
|
|
50
|
+
) -> DeviceAuthResult:
|
|
51
|
+
"""Poll until the authorization is approved, denied, or expired.
|
|
52
|
+
|
|
53
|
+
Calls `on_pending()` (if callable) on each pending response.
|
|
54
|
+
Raises APIError on denial, expiry, or timeout.
|
|
55
|
+
"""
|
|
56
|
+
deadline = time.time() + timeout
|
|
57
|
+
while time.time() < deadline:
|
|
58
|
+
try:
|
|
59
|
+
data = client.post("/api/cli/auth/device/poll/", json={"device_code": device_code})
|
|
60
|
+
return DeviceAuthResult(
|
|
61
|
+
access_token=data["access_token"],
|
|
62
|
+
token_type=data["token_type"],
|
|
63
|
+
token_id=data["token_id"],
|
|
64
|
+
user=data["user"],
|
|
65
|
+
)
|
|
66
|
+
except APIError as exc:
|
|
67
|
+
err_data = getattr(exc, "data", {}) or {}
|
|
68
|
+
err_code = err_data.get("error", "")
|
|
69
|
+
if err_code == "authorization_pending":
|
|
70
|
+
if callable(on_pending):
|
|
71
|
+
on_pending() # type: ignore[call-arg]
|
|
72
|
+
time.sleep(interval)
|
|
73
|
+
continue
|
|
74
|
+
if err_code == "expired_token":
|
|
75
|
+
raise APIError("Authorization code expired. Run `pm login` to try again.")
|
|
76
|
+
if err_code == "access_denied":
|
|
77
|
+
raise APIError("Authorization was denied in the browser.")
|
|
78
|
+
if err_code == "authorization_consumed":
|
|
79
|
+
raise APIError("This authorization code was already used.")
|
|
80
|
+
raise
|
|
81
|
+
raise APIError("Authorization timed out. Run `pm login` to try again.")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def revoke_token(client: APIClient) -> bool:
|
|
85
|
+
data = client.post("/api/cli/auth/logout/")
|
|
86
|
+
return bool(data.get("revoked"))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def whoami(client: APIClient) -> dict:
|
|
90
|
+
return client.get("/api/cli/whoami/")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def auth_status(client: APIClient) -> dict:
|
|
94
|
+
return client.get("/api/cli/auth/status/")
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""HTTPX-based API client.
|
|
2
|
+
|
|
3
|
+
Injects:
|
|
4
|
+
- Authorization: Bearer <token>
|
|
5
|
+
- X-Project-Memory-Session: <session_id> (when provided)
|
|
6
|
+
- User-Agent: projectmemory-cli/<version>
|
|
7
|
+
- Content-Type: application/json
|
|
8
|
+
|
|
9
|
+
All requests go through _request() which translates HTTP errors into
|
|
10
|
+
structured APIError subclasses via api.errors.raise_for_response().
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
|
|
18
|
+
from projectmemory_cli import output
|
|
19
|
+
from projectmemory_cli.api.errors import NetworkError, raise_for_response
|
|
20
|
+
|
|
21
|
+
_VERSION = "0.1.0"
|
|
22
|
+
_TIMEOUT = 30.0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class APIClient:
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
base_url: str,
|
|
29
|
+
token: str | None = None,
|
|
30
|
+
session_id: int | str | None = None,
|
|
31
|
+
debug: bool = False,
|
|
32
|
+
):
|
|
33
|
+
self.base_url = base_url.rstrip("/")
|
|
34
|
+
self.token = token
|
|
35
|
+
self.session_id = session_id
|
|
36
|
+
self.debug = debug
|
|
37
|
+
|
|
38
|
+
def _headers(self, extra: dict | None = None) -> dict[str, str]:
|
|
39
|
+
headers: dict[str, str] = {
|
|
40
|
+
"User-Agent": f"projectmemory-cli/{_VERSION}",
|
|
41
|
+
"Accept": "application/json",
|
|
42
|
+
}
|
|
43
|
+
if self.token:
|
|
44
|
+
headers["Authorization"] = f"Bearer {self.token}"
|
|
45
|
+
if self.session_id is not None:
|
|
46
|
+
headers["X-Project-Memory-Session"] = str(self.session_id)
|
|
47
|
+
if extra:
|
|
48
|
+
headers.update(extra)
|
|
49
|
+
return headers
|
|
50
|
+
|
|
51
|
+
def _log_request(self, method: str, url: str, body: Any = None) -> None:
|
|
52
|
+
if not self.debug:
|
|
53
|
+
return
|
|
54
|
+
token_hint = " [token=pmcli_***]" if self.token else ""
|
|
55
|
+
output.debug(f"{method} {url}{token_hint}")
|
|
56
|
+
if body:
|
|
57
|
+
import json
|
|
58
|
+
output.debug(f" body={json.dumps(body, default=str)[:300]}")
|
|
59
|
+
|
|
60
|
+
def _log_response(self, resp: httpx.Response) -> None:
|
|
61
|
+
if self.debug:
|
|
62
|
+
output.debug(f" -> {resp.status_code}")
|
|
63
|
+
|
|
64
|
+
def _request(
|
|
65
|
+
self,
|
|
66
|
+
method: str,
|
|
67
|
+
path: str,
|
|
68
|
+
*,
|
|
69
|
+
json: Any = None,
|
|
70
|
+
params: dict | None = None,
|
|
71
|
+
project_name: str = "",
|
|
72
|
+
) -> Any:
|
|
73
|
+
url = f"{self.base_url}{path}"
|
|
74
|
+
self._log_request(method, url, json)
|
|
75
|
+
try:
|
|
76
|
+
with httpx.Client(timeout=_TIMEOUT) as client:
|
|
77
|
+
resp = client.request(
|
|
78
|
+
method,
|
|
79
|
+
url,
|
|
80
|
+
headers=self._headers(),
|
|
81
|
+
json=json,
|
|
82
|
+
params=params,
|
|
83
|
+
)
|
|
84
|
+
except httpx.TimeoutException:
|
|
85
|
+
raise NetworkError("Request timed out. Check your connection.")
|
|
86
|
+
except httpx.ConnectError:
|
|
87
|
+
raise NetworkError(
|
|
88
|
+
"Cannot reach the Project Memory API.\n"
|
|
89
|
+
"Check your connection or try again later."
|
|
90
|
+
)
|
|
91
|
+
except httpx.RequestError as exc:
|
|
92
|
+
raise NetworkError(f"Network error: {exc}")
|
|
93
|
+
|
|
94
|
+
self._log_response(resp)
|
|
95
|
+
|
|
96
|
+
if resp.status_code == 204:
|
|
97
|
+
return {}
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
data = resp.json()
|
|
101
|
+
except Exception:
|
|
102
|
+
data = {"detail": resp.text[:500]}
|
|
103
|
+
|
|
104
|
+
if not resp.is_success:
|
|
105
|
+
raise_for_response(resp.status_code, data, project_name=project_name)
|
|
106
|
+
|
|
107
|
+
return data
|
|
108
|
+
|
|
109
|
+
def get(self, path: str, *, params: dict | None = None, project_name: str = "") -> Any:
|
|
110
|
+
return self._request("GET", path, params=params, project_name=project_name)
|
|
111
|
+
|
|
112
|
+
def post(self, path: str, *, json: Any = None, project_name: str = "") -> Any:
|
|
113
|
+
return self._request("POST", path, json=json or {}, project_name=project_name)
|
|
114
|
+
|
|
115
|
+
def patch(self, path: str, *, json: Any = None, project_name: str = "") -> Any:
|
|
116
|
+
return self._request("PATCH", path, json=json or {}, project_name=project_name)
|
|
117
|
+
|
|
118
|
+
def delete(self, path: str, *, project_name: str = "") -> Any:
|
|
119
|
+
return self._request("DELETE", path, project_name=project_name)
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def from_env(
|
|
123
|
+
cls,
|
|
124
|
+
*,
|
|
125
|
+
session_id: int | str | None = None,
|
|
126
|
+
debug: bool = False,
|
|
127
|
+
) -> "APIClient":
|
|
128
|
+
"""Build a client using current config + stored credentials."""
|
|
129
|
+
from projectmemory_cli import config, credentials
|
|
130
|
+
return cls(
|
|
131
|
+
base_url=config.get_api_url(),
|
|
132
|
+
token=credentials.get_token(),
|
|
133
|
+
session_id=session_id,
|
|
134
|
+
debug=debug,
|
|
135
|
+
)
|