quaestor-cli 0.2.1__tar.gz → 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/.gitignore +6 -0
  2. quaestor_cli-0.3.1/PKG-INFO +232 -0
  3. quaestor_cli-0.3.1/README.md +210 -0
  4. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/pyproject.toml +1 -1
  5. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/src/quaestor_cli/client.py +30 -14
  6. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/src/quaestor_cli/commands/doctor.py +68 -15
  7. quaestor_cli-0.3.1/src/quaestor_cli/commands/quest.py +699 -0
  8. quaestor_cli-0.3.1/src/quaestor_cli/commands/quest_format.py +49 -0
  9. quaestor_cli-0.3.1/src/quaestor_cli/commands/quests.py +223 -0
  10. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/src/quaestor_cli/config.py +11 -11
  11. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/src/quaestor_cli/errors.py +5 -1
  12. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/src/quaestor_cli/main.py +25 -3
  13. quaestor_cli-0.3.1/src/quaestor_cli/parse.py +253 -0
  14. quaestor_cli-0.3.1/src/quaestor_cli/quest_api.py +129 -0
  15. quaestor_cli-0.3.1/src/quaestor_cli/quest_models.py +231 -0
  16. quaestor_cli-0.3.1/src/quaestor_cli/runtime.py +172 -0
  17. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/tests/factories.py +36 -0
  18. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/tests/test_cli_auth.py +72 -2
  19. quaestor_cli-0.3.1/tests/test_cli_quest_emoji.py +57 -0
  20. quaestor_cli-0.3.1/tests/test_cli_quests.py +390 -0
  21. quaestor_cli-0.3.1/tests/test_cli_review_fixes.py +373 -0
  22. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/tests/test_cli_timer_review_doctor.py +85 -0
  23. quaestor_cli-0.3.1/tests/test_quest_parse_and_doctor.py +99 -0
  24. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/uv.lock +1 -1
  25. quaestor_cli-0.2.1/PKG-INFO +0 -148
  26. quaestor_cli-0.2.1/README.md +0 -126
  27. quaestor_cli-0.2.1/src/quaestor_cli/parse.py +0 -116
  28. quaestor_cli-0.2.1/src/quaestor_cli/runtime.py +0 -88
  29. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/src/quaestor_cli/__init__.py +0 -0
  30. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/src/quaestor_cli/api.py +0 -0
  31. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/src/quaestor_cli/commands/__init__.py +0 -0
  32. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/src/quaestor_cli/commands/auth.py +0 -0
  33. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/src/quaestor_cli/commands/events.py +0 -0
  34. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/src/quaestor_cli/commands/log.py +0 -0
  35. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/src/quaestor_cli/commands/review.py +0 -0
  36. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/src/quaestor_cli/commands/show.py +0 -0
  37. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/src/quaestor_cli/commands/streaks.py +0 -0
  38. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/src/quaestor_cli/commands/timer.py +0 -0
  39. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/src/quaestor_cli/models.py +0 -0
  40. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/src/quaestor_cli/output.py +0 -0
  41. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/src/quaestor_cli/resolve.py +0 -0
  42. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/src/quaestor_cli/window.py +0 -0
  43. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/tests/conftest.py +0 -0
  44. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/tests/test_cli_errors.py +0 -0
  45. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/tests/test_cli_events.py +0 -0
  46. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/tests/test_cli_log.py +0 -0
  47. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/tests/test_cli_show.py +0 -0
  48. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/tests/test_parse.py +0 -0
  49. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/tests/test_resolve.py +0 -0
  50. {quaestor_cli-0.2.1 → quaestor_cli-0.3.1}/tests/test_window.py +0 -0
@@ -28,4 +28,10 @@ coverage/
28
28
  terraform.tfvars
29
29
  terraform.tfstate
30
30
  .terraform/
31
+
32
+ # Local agent tooling
33
+ .playwright-mcp/
34
+ .claude/
35
+
36
+ # Playwright MCP screenshots and snapshots
31
37
  .playwright-mcp/
@@ -0,0 +1,232 @@
1
+ Metadata-Version: 2.5
2
+ Name: quaestor-cli
3
+ Version: 0.3.1
4
+ Summary: Command-line client for the Quaestor API — built for humans and agents.
5
+ Project-URL: Homepage, https://quaestor.app
6
+ Project-URL: Repository, https://github.com/neuromaxer/quaestor-lite
7
+ Project-URL: Documentation, https://github.com/neuromaxer/quaestor-lite/blob/main/apps/cli/README.md
8
+ Project-URL: Issues, https://github.com/neuromaxer/quaestor-lite/issues
9
+ Keywords: agent,ai,cli,habit-tracker,quaestor
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: End Users/Desktop
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Utilities
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.12
19
+ Requires-Dist: httpx>=0.27
20
+ Requires-Dist: typer>=0.16
21
+ Description-Content-Type: text/markdown
22
+
23
+ # quaestor-cli
24
+
25
+ `qst` — a command-line client for [Quaestor](https://quaestor.app), an agent-native
26
+ platform for diary-based life management. An evening diary becomes tracked habits,
27
+ quest progress, and a morning plan.
28
+
29
+ Built for two users at once: a person in a terminal, and an AI assistant acting on their
30
+ behalf. That second audience shapes most of the design — commands are task-shaped rather
31
+ than endpoint-shaped, streaks and quests are addressed by name, failures are
32
+ distinguishable by exit code, and the server's own error message is printed verbatim so
33
+ an agent can relay something true.
34
+
35
+ ```bash
36
+ uv tool install quaestor-cli
37
+ ```
38
+
39
+ ## Setup
40
+
41
+ Create an API key in the Quaestor web app under **Profile → Developer Access**.
42
+ The CLI uses the hosted production API automatically. On macOS, copy the key and run:
43
+
44
+ ```bash
45
+ pbpaste | qst auth login
46
+ qst doctor
47
+ ```
48
+
49
+ On other systems, run `qst auth login`, paste the key, press Enter, then signal
50
+ end-of-input (Ctrl-D on Unix).
51
+
52
+ The key is stored at `~/.config/quaestor/credentials.json` with mode `0600`. The
53
+ CLI also supports `QUAESTOR_API_KEY`; an existing environment variable takes
54
+ precedence over the file, but it is not needed for this setup.
55
+
56
+ Logging in again replaces the key while preserving an existing server override.
57
+ `qst doctor` reports authentication as unknown when the server cannot verify it;
58
+ only an explicit authentication refusal means the key was rejected.
59
+
60
+ ## The evening diary workflow
61
+
62
+ Spend 10–15 minutes writing or recording your day. Your agent uses `qst` to log
63
+ measured habits, record quest progress, and plan the work you want to do tomorrow.
64
+ It reports what changed and prepares a short morning plan with Main and Side Quests.
65
+
66
+ Keep the original diary with your agent or in a place you choose. The CLI stores
67
+ habit records and quest-specific notes in Quaestor, not a general diary entry.
68
+ Those original entries become a record of your progress and a story worth keeping.
69
+
70
+ ## Commands
71
+
72
+ ```
73
+ qst streaks List streaks with mode, unit and target
74
+ qst show <streak> Full configuration plus per-day totals, including missed days
75
+ qst log <streak> <value> Log a COUNT amount
76
+ qst log <streak> --minutes 45 Log a TIME block (45, 45m, 1h30m, 1.5h)
77
+ qst events <streak> Individual entries, newest first, with the source of each
78
+ qst review Totals, active days and current streak per streak
79
+ qst timer start|stop|status Control the single running timer
80
+ qst auth login|whoami|logout Manage the stored credential
81
+ qst doctor Check URL, reachability, credential and granted scopes
82
+
83
+ qst agenda [--week|--days N] Overdue, past deadline, ready to plan, and each day's quests
84
+ qst quests [words] [filters] Find quests: --status, --overdue, --on, --from/--to,
85
+ --deadline-by, --under <quest>, --archived, --sort, --limit
86
+ qst quest show <quest> Status, dates, sub-quest progress, description, latest Quest Log
87
+ qst quest log <quest> Quest Log entries, newest first (--date for one day)
88
+ qst quest add <title> --under <quest> --on fri --due <date> --priority high --emoji 🚀 ...
89
+ qst quest edit <quest> --title --emoji --priority --start/--due (or 'none') --under/--top-level
90
+ qst quest status <quest> <s> backlog | todo | in-progress | done | archived
91
+ qst quest archive <quest> Hide it and its sub-quests (there is no delete)
92
+ qst quest plan <quest> <day>... Plan days; a Backlog quest becomes Todo
93
+ qst quest unplan <quest> <day> Remove a planned day (never demotes)
94
+ qst quest reschedule <q> <from> [to] Move one planned day (to today by default)
95
+ qst quest check <quest> Check off a day (--date, --undo); starts Backlog/Todo quests
96
+ qst quest note <quest> <text> Append to that day's Quest Log entry (--date, --replace)
97
+ qst quest link|unlink <quest> <streak> Link a streak (context only, nothing syncs)
98
+ ```
99
+
100
+ Every command takes `--json` for machine-readable output on stdout. Every write takes
101
+ `--dry-run`, which prints the request and sends nothing.
102
+
103
+ ### Windows
104
+
105
+ `events`, `show` and `review` share one set of window flags: `--today`, `--week`
106
+ (default), `--days N`, or an explicit `--from` / `--to`. The explicit and relative forms
107
+ cannot be combined. Dates accept `today`, `yesterday`, a weekday (`mon`..`sun`, meaning
108
+ the most recent past one), `YYYY-MM-DD`, or an offset like `-3d`.
109
+
110
+ ### Streaks are named, never UUIDs
111
+
112
+ `<streak>` is a name. Resolution tries exact match, then unique prefix, then unique
113
+ substring. On zero or multiple matches it exits `3`, lists the candidates, and writes
114
+ nothing — it never guesses.
115
+
116
+ ```
117
+ $ qst log r 1
118
+ Streak 'r' is ambiguous — candidates: Reading, Running. Nothing was written.
119
+ ```
120
+
121
+ ### Quests are named too — by title or path
122
+
123
+ `<quest>` is a title, or a path of titles that names the parents as well:
124
+ `"Launch > Backend > Schema"` (`›` works too). The server resolves it — exact, then unique
125
+ prefix, then unique substring; archived quests only when nothing live matches — and on
126
+ zero or several matches `qst` exits `3`, prints the candidates' full paths, and writes
127
+ nothing.
128
+
129
+ ```
130
+ $ qst quest check "write migration"
131
+ Quest 'write migration' is ambiguous — 2 quests match: Launch › Backend › Write migration;
132
+ Blog › Write migration. Use a path such as 'Launch > Backend > Write migration' to pick one.
133
+ Nothing was written.
134
+ ```
135
+
136
+ With `--json`, the failure is one JSON document on stdout: `{"error": {"exit_code": 3,
137
+ "message": ..., "candidates": [...]}}`.
138
+
139
+ ### Which way date words point
140
+
141
+ Where you plan (`add --on/--due/--start`, `plan`, a reschedule target, `agenda`/`quests
142
+ --from/--to`), a weekday means the **coming** one: `fri` on a Wednesday is this Friday,
143
+ and today if it is Friday; `next fri` is a week later; `+1w` is a week from today.
144
+ Where you record (`check --date`, `note --date`, streak commands), a weekday is the most
145
+ recent one and `last fri` is strictly before today; `check` refuses future days.
146
+ Where you name a day the quest already has (`reschedule`'s first day, `unplan`), a
147
+ weekday picks the quest's own date on that weekday within a week of today, in either
148
+ direction — so `qst quest reschedule report tue thu` works whether Tuesday was yesterday
149
+ or is next week.
150
+
151
+ ### Retrying writes safely
152
+
153
+ Dropped connections are retried automatically with the same idempotency key. If a write
154
+ still fails with exit 4, the message includes the key; re-run with
155
+ `qst --idempotency-key <key> ...` (or set `QUAESTOR_IDEMPOTENCY_KEY`) and the server
156
+ replays the original write instead of creating a duplicate.
157
+
158
+ Quest reads send your local date (`today=`), so overdue and "today" match the web app.
159
+ Writes that change a status automatically say so (`Status: Todo → In progress.`).
160
+
161
+ ### `--unit` is a safety check, not data
162
+
163
+ Units belong to the streak, not the event, so `--unit` is never sent to the API. It
164
+ asserts what you believe the streak measures and fails before writing if you are wrong —
165
+ which is what stops an assistant logging "5 miles" as 5 km.
166
+
167
+ ```
168
+ $ qst log running 5 --unit miles
169
+ 'Running' is measured in km, not miles.
170
+ ```
171
+
172
+ ## Exit codes
173
+
174
+ An agent's only reliable error channel, so they are stable:
175
+
176
+ | Code | Meaning |
177
+ | --- | --- |
178
+ | 0 | Success |
179
+ | 1 | Bad arguments or unparseable input |
180
+ | 2 | Authentication failed, or the key lacks the required scope |
181
+ | 3 | Unknown or ambiguous streak or quest |
182
+ | 4 | Server or network error |
183
+
184
+ On failure the server's `detail` message is printed verbatim to stderr.
185
+
186
+ ## Scopes
187
+
188
+ A key only carries the permissions granted when it was created, and the server enforces
189
+ them. Insufficient scope returns a message naming what is missing, so it is clear whether
190
+ something is a bug or a deliberate restriction:
191
+
192
+ ```
193
+ $ qst log reading 5
194
+ API key is missing required scope(s): events:write
195
+ ```
196
+
197
+ Deleting a streak has no scope at all — it is web-app only, because it also destroys every
198
+ event, total and milestone beneath it. The same goes for deleting a quest or a Quest Log
199
+ entry: `qst` has no delete command, and a key can archive a quest instead.
200
+
201
+ Quest commands need `quests:read` (agenda, lists, show, log) and `quests:write`
202
+ (everything else, archive included). Both are pre-checked when a key is created; older
203
+ keys lack them, and `qst doctor` lists the scopes a key actually holds plus a hint naming
204
+ the quest commands that will fail.
205
+
206
+ ## Using this with an AI assistant
207
+
208
+ The repository ships an [Agent
209
+ Skill](https://github.com/neuromaxer/quaestor-lite/blob/main/skills/quaestor/SKILL.md)
210
+ that teaches an assistant when and how to use these commands, following the
211
+ [agentskills.io](https://agentskills.io) format. Copy it to `~/.agents/skills/` (OpenClaw)
212
+ or `~/.hermes/skills/` (Hermes Agent).
213
+
214
+ Design notes and the security model are in the [feature
215
+ documentation](https://github.com/neuromaxer/quaestor-lite/blob/main/docs/features/agent-connect/agent-connect.md).
216
+
217
+ ## Requirements
218
+
219
+ Python 3.12+. Depends only on `httpx` and `typer` — it speaks HTTP and never imports the
220
+ Quaestor backend, so it installs in seconds and tolerates version skew against the server.
221
+
222
+ ## Developer configuration
223
+
224
+ Use `qst --dev doctor` to target `http://localhost:8000` for that invocation.
225
+ The development flag is hidden from normal help and never persists a local URL.
226
+
227
+ Staging and self-hosted deployments can still use `QUAESTOR_URL` or an existing
228
+ `base_url` in the credential store. Resolution order is `--dev`, environment, stored
229
+ URL, then the hosted production default. Ordinary hosted setup needs no override.
230
+
231
+ `qst` refuses to send a key over plain HTTP except to loopback. Set
232
+ `QUAESTOR_ALLOW_INSECURE=1` only for deliberate plaintext development deployments.
@@ -0,0 +1,210 @@
1
+ # quaestor-cli
2
+
3
+ `qst` — a command-line client for [Quaestor](https://quaestor.app), an agent-native
4
+ platform for diary-based life management. An evening diary becomes tracked habits,
5
+ quest progress, and a morning plan.
6
+
7
+ Built for two users at once: a person in a terminal, and an AI assistant acting on their
8
+ behalf. That second audience shapes most of the design — commands are task-shaped rather
9
+ than endpoint-shaped, streaks and quests are addressed by name, failures are
10
+ distinguishable by exit code, and the server's own error message is printed verbatim so
11
+ an agent can relay something true.
12
+
13
+ ```bash
14
+ uv tool install quaestor-cli
15
+ ```
16
+
17
+ ## Setup
18
+
19
+ Create an API key in the Quaestor web app under **Profile → Developer Access**.
20
+ The CLI uses the hosted production API automatically. On macOS, copy the key and run:
21
+
22
+ ```bash
23
+ pbpaste | qst auth login
24
+ qst doctor
25
+ ```
26
+
27
+ On other systems, run `qst auth login`, paste the key, press Enter, then signal
28
+ end-of-input (Ctrl-D on Unix).
29
+
30
+ The key is stored at `~/.config/quaestor/credentials.json` with mode `0600`. The
31
+ CLI also supports `QUAESTOR_API_KEY`; an existing environment variable takes
32
+ precedence over the file, but it is not needed for this setup.
33
+
34
+ Logging in again replaces the key while preserving an existing server override.
35
+ `qst doctor` reports authentication as unknown when the server cannot verify it;
36
+ only an explicit authentication refusal means the key was rejected.
37
+
38
+ ## The evening diary workflow
39
+
40
+ Spend 10–15 minutes writing or recording your day. Your agent uses `qst` to log
41
+ measured habits, record quest progress, and plan the work you want to do tomorrow.
42
+ It reports what changed and prepares a short morning plan with Main and Side Quests.
43
+
44
+ Keep the original diary with your agent or in a place you choose. The CLI stores
45
+ habit records and quest-specific notes in Quaestor, not a general diary entry.
46
+ Those original entries become a record of your progress and a story worth keeping.
47
+
48
+ ## Commands
49
+
50
+ ```
51
+ qst streaks List streaks with mode, unit and target
52
+ qst show <streak> Full configuration plus per-day totals, including missed days
53
+ qst log <streak> <value> Log a COUNT amount
54
+ qst log <streak> --minutes 45 Log a TIME block (45, 45m, 1h30m, 1.5h)
55
+ qst events <streak> Individual entries, newest first, with the source of each
56
+ qst review Totals, active days and current streak per streak
57
+ qst timer start|stop|status Control the single running timer
58
+ qst auth login|whoami|logout Manage the stored credential
59
+ qst doctor Check URL, reachability, credential and granted scopes
60
+
61
+ qst agenda [--week|--days N] Overdue, past deadline, ready to plan, and each day's quests
62
+ qst quests [words] [filters] Find quests: --status, --overdue, --on, --from/--to,
63
+ --deadline-by, --under <quest>, --archived, --sort, --limit
64
+ qst quest show <quest> Status, dates, sub-quest progress, description, latest Quest Log
65
+ qst quest log <quest> Quest Log entries, newest first (--date for one day)
66
+ qst quest add <title> --under <quest> --on fri --due <date> --priority high --emoji 🚀 ...
67
+ qst quest edit <quest> --title --emoji --priority --start/--due (or 'none') --under/--top-level
68
+ qst quest status <quest> <s> backlog | todo | in-progress | done | archived
69
+ qst quest archive <quest> Hide it and its sub-quests (there is no delete)
70
+ qst quest plan <quest> <day>... Plan days; a Backlog quest becomes Todo
71
+ qst quest unplan <quest> <day> Remove a planned day (never demotes)
72
+ qst quest reschedule <q> <from> [to] Move one planned day (to today by default)
73
+ qst quest check <quest> Check off a day (--date, --undo); starts Backlog/Todo quests
74
+ qst quest note <quest> <text> Append to that day's Quest Log entry (--date, --replace)
75
+ qst quest link|unlink <quest> <streak> Link a streak (context only, nothing syncs)
76
+ ```
77
+
78
+ Every command takes `--json` for machine-readable output on stdout. Every write takes
79
+ `--dry-run`, which prints the request and sends nothing.
80
+
81
+ ### Windows
82
+
83
+ `events`, `show` and `review` share one set of window flags: `--today`, `--week`
84
+ (default), `--days N`, or an explicit `--from` / `--to`. The explicit and relative forms
85
+ cannot be combined. Dates accept `today`, `yesterday`, a weekday (`mon`..`sun`, meaning
86
+ the most recent past one), `YYYY-MM-DD`, or an offset like `-3d`.
87
+
88
+ ### Streaks are named, never UUIDs
89
+
90
+ `<streak>` is a name. Resolution tries exact match, then unique prefix, then unique
91
+ substring. On zero or multiple matches it exits `3`, lists the candidates, and writes
92
+ nothing — it never guesses.
93
+
94
+ ```
95
+ $ qst log r 1
96
+ Streak 'r' is ambiguous — candidates: Reading, Running. Nothing was written.
97
+ ```
98
+
99
+ ### Quests are named too — by title or path
100
+
101
+ `<quest>` is a title, or a path of titles that names the parents as well:
102
+ `"Launch > Backend > Schema"` (`›` works too). The server resolves it — exact, then unique
103
+ prefix, then unique substring; archived quests only when nothing live matches — and on
104
+ zero or several matches `qst` exits `3`, prints the candidates' full paths, and writes
105
+ nothing.
106
+
107
+ ```
108
+ $ qst quest check "write migration"
109
+ Quest 'write migration' is ambiguous — 2 quests match: Launch › Backend › Write migration;
110
+ Blog › Write migration. Use a path such as 'Launch > Backend > Write migration' to pick one.
111
+ Nothing was written.
112
+ ```
113
+
114
+ With `--json`, the failure is one JSON document on stdout: `{"error": {"exit_code": 3,
115
+ "message": ..., "candidates": [...]}}`.
116
+
117
+ ### Which way date words point
118
+
119
+ Where you plan (`add --on/--due/--start`, `plan`, a reschedule target, `agenda`/`quests
120
+ --from/--to`), a weekday means the **coming** one: `fri` on a Wednesday is this Friday,
121
+ and today if it is Friday; `next fri` is a week later; `+1w` is a week from today.
122
+ Where you record (`check --date`, `note --date`, streak commands), a weekday is the most
123
+ recent one and `last fri` is strictly before today; `check` refuses future days.
124
+ Where you name a day the quest already has (`reschedule`'s first day, `unplan`), a
125
+ weekday picks the quest's own date on that weekday within a week of today, in either
126
+ direction — so `qst quest reschedule report tue thu` works whether Tuesday was yesterday
127
+ or is next week.
128
+
129
+ ### Retrying writes safely
130
+
131
+ Dropped connections are retried automatically with the same idempotency key. If a write
132
+ still fails with exit 4, the message includes the key; re-run with
133
+ `qst --idempotency-key <key> ...` (or set `QUAESTOR_IDEMPOTENCY_KEY`) and the server
134
+ replays the original write instead of creating a duplicate.
135
+
136
+ Quest reads send your local date (`today=`), so overdue and "today" match the web app.
137
+ Writes that change a status automatically say so (`Status: Todo → In progress.`).
138
+
139
+ ### `--unit` is a safety check, not data
140
+
141
+ Units belong to the streak, not the event, so `--unit` is never sent to the API. It
142
+ asserts what you believe the streak measures and fails before writing if you are wrong —
143
+ which is what stops an assistant logging "5 miles" as 5 km.
144
+
145
+ ```
146
+ $ qst log running 5 --unit miles
147
+ 'Running' is measured in km, not miles.
148
+ ```
149
+
150
+ ## Exit codes
151
+
152
+ An agent's only reliable error channel, so they are stable:
153
+
154
+ | Code | Meaning |
155
+ | --- | --- |
156
+ | 0 | Success |
157
+ | 1 | Bad arguments or unparseable input |
158
+ | 2 | Authentication failed, or the key lacks the required scope |
159
+ | 3 | Unknown or ambiguous streak or quest |
160
+ | 4 | Server or network error |
161
+
162
+ On failure the server's `detail` message is printed verbatim to stderr.
163
+
164
+ ## Scopes
165
+
166
+ A key only carries the permissions granted when it was created, and the server enforces
167
+ them. Insufficient scope returns a message naming what is missing, so it is clear whether
168
+ something is a bug or a deliberate restriction:
169
+
170
+ ```
171
+ $ qst log reading 5
172
+ API key is missing required scope(s): events:write
173
+ ```
174
+
175
+ Deleting a streak has no scope at all — it is web-app only, because it also destroys every
176
+ event, total and milestone beneath it. The same goes for deleting a quest or a Quest Log
177
+ entry: `qst` has no delete command, and a key can archive a quest instead.
178
+
179
+ Quest commands need `quests:read` (agenda, lists, show, log) and `quests:write`
180
+ (everything else, archive included). Both are pre-checked when a key is created; older
181
+ keys lack them, and `qst doctor` lists the scopes a key actually holds plus a hint naming
182
+ the quest commands that will fail.
183
+
184
+ ## Using this with an AI assistant
185
+
186
+ The repository ships an [Agent
187
+ Skill](https://github.com/neuromaxer/quaestor-lite/blob/main/skills/quaestor/SKILL.md)
188
+ that teaches an assistant when and how to use these commands, following the
189
+ [agentskills.io](https://agentskills.io) format. Copy it to `~/.agents/skills/` (OpenClaw)
190
+ or `~/.hermes/skills/` (Hermes Agent).
191
+
192
+ Design notes and the security model are in the [feature
193
+ documentation](https://github.com/neuromaxer/quaestor-lite/blob/main/docs/features/agent-connect/agent-connect.md).
194
+
195
+ ## Requirements
196
+
197
+ Python 3.12+. Depends only on `httpx` and `typer` — it speaks HTTP and never imports the
198
+ Quaestor backend, so it installs in seconds and tolerates version skew against the server.
199
+
200
+ ## Developer configuration
201
+
202
+ Use `qst --dev doctor` to target `http://localhost:8000` for that invocation.
203
+ The development flag is hidden from normal help and never persists a local URL.
204
+
205
+ Staging and self-hosted deployments can still use `QUAESTOR_URL` or an existing
206
+ `base_url` in the credential store. Resolution order is `--dev`, environment, stored
207
+ URL, then the hosted production default. Ordinary hosted setup needs no override.
208
+
209
+ `qst` refuses to send a key over plain HTTP except to loopback. Set
210
+ `QUAESTOR_ALLOW_INSECURE=1` only for deliberate plaintext development deployments.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "quaestor-cli"
3
- version = "0.2.1"
3
+ version = "0.3.1"
4
4
  description = "Command-line client for the Quaestor API — built for humans and agents."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -6,6 +6,7 @@ level adds ~18ms to every invocation, including `qst --help`.
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
+ import time
9
10
  from collections.abc import Mapping
10
11
  from dataclasses import dataclass
11
12
  from typing import Any
@@ -15,6 +16,7 @@ from quaestor_cli.errors import CliError, ExitCode
15
16
 
16
17
  API_PREFIX = "/api/v1"
17
18
  DEFAULT_TIMEOUT_SECONDS = 15.0
19
+ RETRY_BACKOFF_SECONDS = 0.5 # 0.5s, then 1s: enough to ride out a blip, short for an agent turn
18
20
 
19
21
  # Maps an HTTP failure onto the exit code an agent should act on.
20
22
  _STATUS_EXIT_CODES: dict[int, ExitCode] = {
@@ -135,26 +137,40 @@ class QuaestorClient:
135
137
  params: Mapping[str, Any] | None = None,
136
138
  body: Mapping[str, Any] | None = None,
137
139
  check: bool = True,
140
+ retries: int = 0,
138
141
  ) -> ApiResponse:
139
- """Send one request, mapping transport and HTTP failures to CliError."""
142
+ """Send one request, mapping transport and HTTP failures to CliError.
143
+
144
+ `retries` re-sends the *same* request after a transport failure (timeout, dropped
145
+ connection), never after an HTTP error. Only pass it for requests that are safe to
146
+ repeat: idempotent ones, or writes carrying an idempotency key.
147
+ """
140
148
  import httpx # Lazy: keeps `qst --help` fast.
141
149
 
142
150
  url = self.url_for(path)
143
- try:
144
- raw = httpx.request(
145
- method,
146
- url,
147
- params=_clean_params(params),
148
- json=dict(body) if body is not None else None,
149
- headers=self._headers(),
150
- timeout=self._timeout,
151
- )
152
- except httpx.HTTPError as exc:
153
- raise CliError(f"Cannot reach {url}: {exc}", ExitCode.SERVER) from exc
151
+ attempt = 0
152
+ while True:
153
+ try:
154
+ raw = httpx.request(
155
+ method,
156
+ url,
157
+ params=_clean_params(params),
158
+ json=dict(body) if body is not None else None,
159
+ headers=self._headers(),
160
+ timeout=self._timeout,
161
+ )
162
+ break
163
+ except httpx.TransportError as exc:
164
+ if attempt >= retries:
165
+ raise CliError(f"Cannot reach {url}: {exc}", ExitCode.SERVER) from exc
166
+ attempt += 1
167
+ time.sleep(RETRY_BACKOFF_SECONDS * attempt)
168
+ except httpx.HTTPError as exc:
169
+ raise CliError(f"Cannot reach {url}: {exc}", ExitCode.SERVER) from exc
154
170
 
155
171
  response = ApiResponse(status_code=raw.status_code, payload=_decode(raw))
156
172
  if check and not response.is_success:
157
- raise CliError(response.detail, _exit_code_for(response.status_code))
173
+ raise CliError(response.detail, exit_code_for(response.status_code))
158
174
  return response
159
175
 
160
176
  def _headers(self) -> dict[str, str]:
@@ -165,7 +181,7 @@ class QuaestorClient:
165
181
  return headers
166
182
 
167
183
 
168
- def _exit_code_for(status_code: int) -> ExitCode:
184
+ def exit_code_for(status_code: int) -> ExitCode:
169
185
  """Translate an HTTP status into the CLI's exit-code contract."""
170
186
  return _STATUS_EXIT_CODES.get(status_code, ExitCode.SERVER)
171
187