quaestor-cli 0.2.0__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.
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/.gitignore +6 -0
- quaestor_cli-0.3.0/PKG-INFO +221 -0
- quaestor_cli-0.3.0/README.md +199 -0
- quaestor_cli-0.3.0/pyproject.toml +69 -0
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/src/quaestor_cli/client.py +30 -14
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/src/quaestor_cli/commands/doctor.py +34 -6
- quaestor_cli-0.3.0/src/quaestor_cli/commands/quest.py +699 -0
- quaestor_cli-0.3.0/src/quaestor_cli/commands/quest_format.py +49 -0
- quaestor_cli-0.3.0/src/quaestor_cli/commands/quests.py +223 -0
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/src/quaestor_cli/errors.py +5 -1
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/src/quaestor_cli/main.py +24 -3
- quaestor_cli-0.3.0/src/quaestor_cli/parse.py +253 -0
- quaestor_cli-0.3.0/src/quaestor_cli/quest_api.py +129 -0
- quaestor_cli-0.3.0/src/quaestor_cli/quest_models.py +231 -0
- quaestor_cli-0.3.0/src/quaestor_cli/runtime.py +171 -0
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/tests/factories.py +36 -0
- quaestor_cli-0.3.0/tests/test_cli_quest_emoji.py +57 -0
- quaestor_cli-0.3.0/tests/test_cli_quests.py +390 -0
- quaestor_cli-0.3.0/tests/test_cli_review_fixes.py +373 -0
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/tests/test_cli_timer_review_doctor.py +17 -0
- quaestor_cli-0.3.0/tests/test_quest_parse_and_doctor.py +99 -0
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/uv.lock +2 -2
- quaestor_cli-0.2.0/PKG-INFO +0 -7
- quaestor_cli-0.2.0/pyproject.toml +0 -45
- quaestor_cli-0.2.0/src/quaestor_cli/parse.py +0 -116
- quaestor_cli-0.2.0/src/quaestor_cli/runtime.py +0 -88
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/src/quaestor_cli/__init__.py +0 -0
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/src/quaestor_cli/api.py +0 -0
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/src/quaestor_cli/commands/__init__.py +0 -0
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/src/quaestor_cli/commands/auth.py +0 -0
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/src/quaestor_cli/commands/events.py +0 -0
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/src/quaestor_cli/commands/log.py +0 -0
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/src/quaestor_cli/commands/review.py +0 -0
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/src/quaestor_cli/commands/show.py +0 -0
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/src/quaestor_cli/commands/streaks.py +0 -0
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/src/quaestor_cli/commands/timer.py +0 -0
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/src/quaestor_cli/config.py +0 -0
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/src/quaestor_cli/models.py +0 -0
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/src/quaestor_cli/output.py +0 -0
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/src/quaestor_cli/resolve.py +0 -0
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/src/quaestor_cli/window.py +0 -0
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/tests/conftest.py +0 -0
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/tests/test_cli_auth.py +0 -0
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/tests/test_cli_errors.py +0 -0
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/tests/test_cli_events.py +0 -0
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/tests/test_cli_log.py +0 -0
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/tests/test_cli_show.py +0 -0
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/tests/test_parse.py +0 -0
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/tests/test_resolve.py +0 -0
- {quaestor_cli-0.2.0 → quaestor_cli-0.3.0}/tests/test_window.py +0 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: quaestor-cli
|
|
3
|
+
Version: 0.3.0
|
|
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
|
+
Set the backend URL, then store the key from stdin. On macOS, copy the key and run:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
export QUAESTOR_URL="https://your-quaestor-backend" # defaults to http://localhost:8000
|
|
46
|
+
pbpaste | qst auth login
|
|
47
|
+
qst doctor
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
On other systems, run `qst auth login`, paste the key, press Enter, then signal
|
|
51
|
+
end-of-input (Ctrl-D on Unix).
|
|
52
|
+
|
|
53
|
+
The key is stored at `~/.config/quaestor/credentials.json` with mode `0600`. The
|
|
54
|
+
CLI also supports `QUAESTOR_API_KEY`; an existing environment variable takes
|
|
55
|
+
precedence over the file, but it is not needed for this setup.
|
|
56
|
+
|
|
57
|
+
`qst` refuses to send a key over plain HTTP to anything but a loopback host. Set
|
|
58
|
+
`QUAESTOR_ALLOW_INSECURE=1` only if you are knowingly running a plaintext self-hosted
|
|
59
|
+
backend.
|
|
60
|
+
|
|
61
|
+
## The evening diary workflow
|
|
62
|
+
|
|
63
|
+
Spend 10–15 minutes writing or recording your day. Your agent uses `qst` to log
|
|
64
|
+
measured habits, record quest progress, and plan the work you want to do tomorrow.
|
|
65
|
+
It reports what changed and prepares a short morning plan with Main and Side Quests.
|
|
66
|
+
|
|
67
|
+
Keep the original diary with your agent or in a place you choose. The CLI stores
|
|
68
|
+
habit records and quest-specific notes in Quaestor, not a general diary entry.
|
|
69
|
+
Those original entries become a record of your progress and a story worth keeping.
|
|
70
|
+
|
|
71
|
+
## Commands
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
qst streaks List streaks with mode, unit and target
|
|
75
|
+
qst show <streak> Full configuration plus per-day totals, including missed days
|
|
76
|
+
qst log <streak> <value> Log a COUNT amount
|
|
77
|
+
qst log <streak> --minutes 45 Log a TIME block (45, 45m, 1h30m, 1.5h)
|
|
78
|
+
qst events <streak> Individual entries, newest first, with the source of each
|
|
79
|
+
qst review Totals, active days and current streak per streak
|
|
80
|
+
qst timer start|stop|status Control the single running timer
|
|
81
|
+
qst auth login|whoami|logout Manage the stored credential
|
|
82
|
+
qst doctor Check URL, reachability, credential and granted scopes
|
|
83
|
+
|
|
84
|
+
qst agenda [--week|--days N] Overdue, past deadline, ready to plan, and each day's quests
|
|
85
|
+
qst quests [words] [filters] Find quests: --status, --overdue, --on, --from/--to,
|
|
86
|
+
--deadline-by, --under <quest>, --archived, --sort, --limit
|
|
87
|
+
qst quest show <quest> Status, dates, sub-quest progress, description, latest Quest Log
|
|
88
|
+
qst quest log <quest> Quest Log entries, newest first (--date for one day)
|
|
89
|
+
qst quest add <title> --under <quest> --on fri --due <date> --priority high --emoji 🚀 ...
|
|
90
|
+
qst quest edit <quest> --title --emoji --priority --start/--due (or 'none') --under/--top-level
|
|
91
|
+
qst quest status <quest> <s> backlog | todo | in-progress | done | archived
|
|
92
|
+
qst quest archive <quest> Hide it and its sub-quests (there is no delete)
|
|
93
|
+
qst quest plan <quest> <day>... Plan days; a Backlog quest becomes Todo
|
|
94
|
+
qst quest unplan <quest> <day> Remove a planned day (never demotes)
|
|
95
|
+
qst quest reschedule <q> <from> [to] Move one planned day (to today by default)
|
|
96
|
+
qst quest check <quest> Check off a day (--date, --undo); starts Backlog/Todo quests
|
|
97
|
+
qst quest note <quest> <text> Append to that day's Quest Log entry (--date, --replace)
|
|
98
|
+
qst quest link|unlink <quest> <streak> Link a streak (context only, nothing syncs)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Every command takes `--json` for machine-readable output on stdout. Every write takes
|
|
102
|
+
`--dry-run`, which prints the request and sends nothing.
|
|
103
|
+
|
|
104
|
+
### Windows
|
|
105
|
+
|
|
106
|
+
`events`, `show` and `review` share one set of window flags: `--today`, `--week`
|
|
107
|
+
(default), `--days N`, or an explicit `--from` / `--to`. The explicit and relative forms
|
|
108
|
+
cannot be combined. Dates accept `today`, `yesterday`, a weekday (`mon`..`sun`, meaning
|
|
109
|
+
the most recent past one), `YYYY-MM-DD`, or an offset like `-3d`.
|
|
110
|
+
|
|
111
|
+
### Streaks are named, never UUIDs
|
|
112
|
+
|
|
113
|
+
`<streak>` is a name. Resolution tries exact match, then unique prefix, then unique
|
|
114
|
+
substring. On zero or multiple matches it exits `3`, lists the candidates, and writes
|
|
115
|
+
nothing — it never guesses.
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
$ qst log r 1
|
|
119
|
+
Streak 'r' is ambiguous — candidates: Reading, Running. Nothing was written.
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Quests are named too — by title or path
|
|
123
|
+
|
|
124
|
+
`<quest>` is a title, or a path of titles that names the parents as well:
|
|
125
|
+
`"Launch > Backend > Schema"` (`›` works too). The server resolves it — exact, then unique
|
|
126
|
+
prefix, then unique substring; archived quests only when nothing live matches — and on
|
|
127
|
+
zero or several matches `qst` exits `3`, prints the candidates' full paths, and writes
|
|
128
|
+
nothing.
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
$ qst quest check "write migration"
|
|
132
|
+
Quest 'write migration' is ambiguous — 2 quests match: Launch › Backend › Write migration;
|
|
133
|
+
Blog › Write migration. Use a path such as 'Launch > Backend > Write migration' to pick one.
|
|
134
|
+
Nothing was written.
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
With `--json`, the failure is one JSON document on stdout: `{"error": {"exit_code": 3,
|
|
138
|
+
"message": ..., "candidates": [...]}}`.
|
|
139
|
+
|
|
140
|
+
### Which way date words point
|
|
141
|
+
|
|
142
|
+
Where you plan (`add --on/--due/--start`, `plan`, a reschedule target, `agenda`/`quests
|
|
143
|
+
--from/--to`), a weekday means the **coming** one: `fri` on a Wednesday is this Friday,
|
|
144
|
+
and today if it is Friday; `next fri` is a week later; `+1w` is a week from today.
|
|
145
|
+
Where you record (`check --date`, `note --date`, streak commands), a weekday is the most
|
|
146
|
+
recent one and `last fri` is strictly before today; `check` refuses future days.
|
|
147
|
+
Where you name a day the quest already has (`reschedule`'s first day, `unplan`), a
|
|
148
|
+
weekday picks the quest's own date on that weekday within a week of today, in either
|
|
149
|
+
direction — so `qst quest reschedule report tue thu` works whether Tuesday was yesterday
|
|
150
|
+
or is next week.
|
|
151
|
+
|
|
152
|
+
### Retrying writes safely
|
|
153
|
+
|
|
154
|
+
Dropped connections are retried automatically with the same idempotency key. If a write
|
|
155
|
+
still fails with exit 4, the message includes the key; re-run with
|
|
156
|
+
`qst --idempotency-key <key> ...` (or set `QUAESTOR_IDEMPOTENCY_KEY`) and the server
|
|
157
|
+
replays the original write instead of creating a duplicate.
|
|
158
|
+
|
|
159
|
+
Quest reads send your local date (`today=`), so overdue and "today" match the web app.
|
|
160
|
+
Writes that change a status automatically say so (`Status: Todo → In progress.`).
|
|
161
|
+
|
|
162
|
+
### `--unit` is a safety check, not data
|
|
163
|
+
|
|
164
|
+
Units belong to the streak, not the event, so `--unit` is never sent to the API. It
|
|
165
|
+
asserts what you believe the streak measures and fails before writing if you are wrong —
|
|
166
|
+
which is what stops an assistant logging "5 miles" as 5 km.
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
$ qst log running 5 --unit miles
|
|
170
|
+
'Running' is measured in km, not miles.
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Exit codes
|
|
174
|
+
|
|
175
|
+
An agent's only reliable error channel, so they are stable:
|
|
176
|
+
|
|
177
|
+
| Code | Meaning |
|
|
178
|
+
| --- | --- |
|
|
179
|
+
| 0 | Success |
|
|
180
|
+
| 1 | Bad arguments or unparseable input |
|
|
181
|
+
| 2 | Authentication failed, or the key lacks the required scope |
|
|
182
|
+
| 3 | Unknown or ambiguous streak or quest |
|
|
183
|
+
| 4 | Server or network error |
|
|
184
|
+
|
|
185
|
+
On failure the server's `detail` message is printed verbatim to stderr.
|
|
186
|
+
|
|
187
|
+
## Scopes
|
|
188
|
+
|
|
189
|
+
A key only carries the permissions granted when it was created, and the server enforces
|
|
190
|
+
them. Insufficient scope returns a message naming what is missing, so it is clear whether
|
|
191
|
+
something is a bug or a deliberate restriction:
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
$ qst log reading 5
|
|
195
|
+
API key is missing required scope(s): events:write
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Deleting a streak has no scope at all — it is web-app only, because it also destroys every
|
|
199
|
+
event, total and milestone beneath it. The same goes for deleting a quest or a Quest Log
|
|
200
|
+
entry: `qst` has no delete command, and a key can archive a quest instead.
|
|
201
|
+
|
|
202
|
+
Quest commands need `quests:read` (agenda, lists, show, log) and `quests:write`
|
|
203
|
+
(everything else, archive included). Both are pre-checked when a key is created; older
|
|
204
|
+
keys lack them, and `qst doctor` lists the scopes a key actually holds plus a hint naming
|
|
205
|
+
the quest commands that will fail.
|
|
206
|
+
|
|
207
|
+
## Using this with an AI assistant
|
|
208
|
+
|
|
209
|
+
The repository ships an [Agent
|
|
210
|
+
Skill](https://github.com/neuromaxer/quaestor-lite/blob/main/skills/quaestor/SKILL.md)
|
|
211
|
+
that teaches an assistant when and how to use these commands, following the
|
|
212
|
+
[agentskills.io](https://agentskills.io) format. Copy it to `~/.agents/skills/` (OpenClaw)
|
|
213
|
+
or `~/.hermes/skills/` (Hermes Agent).
|
|
214
|
+
|
|
215
|
+
Design notes and the security model are in the [feature
|
|
216
|
+
documentation](https://github.com/neuromaxer/quaestor-lite/blob/main/docs/features/agent-connect/agent-connect.md).
|
|
217
|
+
|
|
218
|
+
## Requirements
|
|
219
|
+
|
|
220
|
+
Python 3.12+. Depends only on `httpx` and `typer` — it speaks HTTP and never imports the
|
|
221
|
+
Quaestor backend, so it installs in seconds and tolerates version skew against the server.
|
|
@@ -0,0 +1,199 @@
|
|
|
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
|
+
Set the backend URL, then store the key from stdin. On macOS, copy the key and run:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
export QUAESTOR_URL="https://your-quaestor-backend" # defaults to http://localhost:8000
|
|
24
|
+
pbpaste | qst auth login
|
|
25
|
+
qst doctor
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
On other systems, run `qst auth login`, paste the key, press Enter, then signal
|
|
29
|
+
end-of-input (Ctrl-D on Unix).
|
|
30
|
+
|
|
31
|
+
The key is stored at `~/.config/quaestor/credentials.json` with mode `0600`. The
|
|
32
|
+
CLI also supports `QUAESTOR_API_KEY`; an existing environment variable takes
|
|
33
|
+
precedence over the file, but it is not needed for this setup.
|
|
34
|
+
|
|
35
|
+
`qst` refuses to send a key over plain HTTP to anything but a loopback host. Set
|
|
36
|
+
`QUAESTOR_ALLOW_INSECURE=1` only if you are knowingly running a plaintext self-hosted
|
|
37
|
+
backend.
|
|
38
|
+
|
|
39
|
+
## The evening diary workflow
|
|
40
|
+
|
|
41
|
+
Spend 10–15 minutes writing or recording your day. Your agent uses `qst` to log
|
|
42
|
+
measured habits, record quest progress, and plan the work you want to do tomorrow.
|
|
43
|
+
It reports what changed and prepares a short morning plan with Main and Side Quests.
|
|
44
|
+
|
|
45
|
+
Keep the original diary with your agent or in a place you choose. The CLI stores
|
|
46
|
+
habit records and quest-specific notes in Quaestor, not a general diary entry.
|
|
47
|
+
Those original entries become a record of your progress and a story worth keeping.
|
|
48
|
+
|
|
49
|
+
## Commands
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
qst streaks List streaks with mode, unit and target
|
|
53
|
+
qst show <streak> Full configuration plus per-day totals, including missed days
|
|
54
|
+
qst log <streak> <value> Log a COUNT amount
|
|
55
|
+
qst log <streak> --minutes 45 Log a TIME block (45, 45m, 1h30m, 1.5h)
|
|
56
|
+
qst events <streak> Individual entries, newest first, with the source of each
|
|
57
|
+
qst review Totals, active days and current streak per streak
|
|
58
|
+
qst timer start|stop|status Control the single running timer
|
|
59
|
+
qst auth login|whoami|logout Manage the stored credential
|
|
60
|
+
qst doctor Check URL, reachability, credential and granted scopes
|
|
61
|
+
|
|
62
|
+
qst agenda [--week|--days N] Overdue, past deadline, ready to plan, and each day's quests
|
|
63
|
+
qst quests [words] [filters] Find quests: --status, --overdue, --on, --from/--to,
|
|
64
|
+
--deadline-by, --under <quest>, --archived, --sort, --limit
|
|
65
|
+
qst quest show <quest> Status, dates, sub-quest progress, description, latest Quest Log
|
|
66
|
+
qst quest log <quest> Quest Log entries, newest first (--date for one day)
|
|
67
|
+
qst quest add <title> --under <quest> --on fri --due <date> --priority high --emoji 🚀 ...
|
|
68
|
+
qst quest edit <quest> --title --emoji --priority --start/--due (or 'none') --under/--top-level
|
|
69
|
+
qst quest status <quest> <s> backlog | todo | in-progress | done | archived
|
|
70
|
+
qst quest archive <quest> Hide it and its sub-quests (there is no delete)
|
|
71
|
+
qst quest plan <quest> <day>... Plan days; a Backlog quest becomes Todo
|
|
72
|
+
qst quest unplan <quest> <day> Remove a planned day (never demotes)
|
|
73
|
+
qst quest reschedule <q> <from> [to] Move one planned day (to today by default)
|
|
74
|
+
qst quest check <quest> Check off a day (--date, --undo); starts Backlog/Todo quests
|
|
75
|
+
qst quest note <quest> <text> Append to that day's Quest Log entry (--date, --replace)
|
|
76
|
+
qst quest link|unlink <quest> <streak> Link a streak (context only, nothing syncs)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Every command takes `--json` for machine-readable output on stdout. Every write takes
|
|
80
|
+
`--dry-run`, which prints the request and sends nothing.
|
|
81
|
+
|
|
82
|
+
### Windows
|
|
83
|
+
|
|
84
|
+
`events`, `show` and `review` share one set of window flags: `--today`, `--week`
|
|
85
|
+
(default), `--days N`, or an explicit `--from` / `--to`. The explicit and relative forms
|
|
86
|
+
cannot be combined. Dates accept `today`, `yesterday`, a weekday (`mon`..`sun`, meaning
|
|
87
|
+
the most recent past one), `YYYY-MM-DD`, or an offset like `-3d`.
|
|
88
|
+
|
|
89
|
+
### Streaks are named, never UUIDs
|
|
90
|
+
|
|
91
|
+
`<streak>` is a name. Resolution tries exact match, then unique prefix, then unique
|
|
92
|
+
substring. On zero or multiple matches it exits `3`, lists the candidates, and writes
|
|
93
|
+
nothing — it never guesses.
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
$ qst log r 1
|
|
97
|
+
Streak 'r' is ambiguous — candidates: Reading, Running. Nothing was written.
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Quests are named too — by title or path
|
|
101
|
+
|
|
102
|
+
`<quest>` is a title, or a path of titles that names the parents as well:
|
|
103
|
+
`"Launch > Backend > Schema"` (`›` works too). The server resolves it — exact, then unique
|
|
104
|
+
prefix, then unique substring; archived quests only when nothing live matches — and on
|
|
105
|
+
zero or several matches `qst` exits `3`, prints the candidates' full paths, and writes
|
|
106
|
+
nothing.
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
$ qst quest check "write migration"
|
|
110
|
+
Quest 'write migration' is ambiguous — 2 quests match: Launch › Backend › Write migration;
|
|
111
|
+
Blog › Write migration. Use a path such as 'Launch > Backend > Write migration' to pick one.
|
|
112
|
+
Nothing was written.
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
With `--json`, the failure is one JSON document on stdout: `{"error": {"exit_code": 3,
|
|
116
|
+
"message": ..., "candidates": [...]}}`.
|
|
117
|
+
|
|
118
|
+
### Which way date words point
|
|
119
|
+
|
|
120
|
+
Where you plan (`add --on/--due/--start`, `plan`, a reschedule target, `agenda`/`quests
|
|
121
|
+
--from/--to`), a weekday means the **coming** one: `fri` on a Wednesday is this Friday,
|
|
122
|
+
and today if it is Friday; `next fri` is a week later; `+1w` is a week from today.
|
|
123
|
+
Where you record (`check --date`, `note --date`, streak commands), a weekday is the most
|
|
124
|
+
recent one and `last fri` is strictly before today; `check` refuses future days.
|
|
125
|
+
Where you name a day the quest already has (`reschedule`'s first day, `unplan`), a
|
|
126
|
+
weekday picks the quest's own date on that weekday within a week of today, in either
|
|
127
|
+
direction — so `qst quest reschedule report tue thu` works whether Tuesday was yesterday
|
|
128
|
+
or is next week.
|
|
129
|
+
|
|
130
|
+
### Retrying writes safely
|
|
131
|
+
|
|
132
|
+
Dropped connections are retried automatically with the same idempotency key. If a write
|
|
133
|
+
still fails with exit 4, the message includes the key; re-run with
|
|
134
|
+
`qst --idempotency-key <key> ...` (or set `QUAESTOR_IDEMPOTENCY_KEY`) and the server
|
|
135
|
+
replays the original write instead of creating a duplicate.
|
|
136
|
+
|
|
137
|
+
Quest reads send your local date (`today=`), so overdue and "today" match the web app.
|
|
138
|
+
Writes that change a status automatically say so (`Status: Todo → In progress.`).
|
|
139
|
+
|
|
140
|
+
### `--unit` is a safety check, not data
|
|
141
|
+
|
|
142
|
+
Units belong to the streak, not the event, so `--unit` is never sent to the API. It
|
|
143
|
+
asserts what you believe the streak measures and fails before writing if you are wrong —
|
|
144
|
+
which is what stops an assistant logging "5 miles" as 5 km.
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
$ qst log running 5 --unit miles
|
|
148
|
+
'Running' is measured in km, not miles.
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Exit codes
|
|
152
|
+
|
|
153
|
+
An agent's only reliable error channel, so they are stable:
|
|
154
|
+
|
|
155
|
+
| Code | Meaning |
|
|
156
|
+
| --- | --- |
|
|
157
|
+
| 0 | Success |
|
|
158
|
+
| 1 | Bad arguments or unparseable input |
|
|
159
|
+
| 2 | Authentication failed, or the key lacks the required scope |
|
|
160
|
+
| 3 | Unknown or ambiguous streak or quest |
|
|
161
|
+
| 4 | Server or network error |
|
|
162
|
+
|
|
163
|
+
On failure the server's `detail` message is printed verbatim to stderr.
|
|
164
|
+
|
|
165
|
+
## Scopes
|
|
166
|
+
|
|
167
|
+
A key only carries the permissions granted when it was created, and the server enforces
|
|
168
|
+
them. Insufficient scope returns a message naming what is missing, so it is clear whether
|
|
169
|
+
something is a bug or a deliberate restriction:
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
$ qst log reading 5
|
|
173
|
+
API key is missing required scope(s): events:write
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Deleting a streak has no scope at all — it is web-app only, because it also destroys every
|
|
177
|
+
event, total and milestone beneath it. The same goes for deleting a quest or a Quest Log
|
|
178
|
+
entry: `qst` has no delete command, and a key can archive a quest instead.
|
|
179
|
+
|
|
180
|
+
Quest commands need `quests:read` (agenda, lists, show, log) and `quests:write`
|
|
181
|
+
(everything else, archive included). Both are pre-checked when a key is created; older
|
|
182
|
+
keys lack them, and `qst doctor` lists the scopes a key actually holds plus a hint naming
|
|
183
|
+
the quest commands that will fail.
|
|
184
|
+
|
|
185
|
+
## Using this with an AI assistant
|
|
186
|
+
|
|
187
|
+
The repository ships an [Agent
|
|
188
|
+
Skill](https://github.com/neuromaxer/quaestor-lite/blob/main/skills/quaestor/SKILL.md)
|
|
189
|
+
that teaches an assistant when and how to use these commands, following the
|
|
190
|
+
[agentskills.io](https://agentskills.io) format. Copy it to `~/.agents/skills/` (OpenClaw)
|
|
191
|
+
or `~/.hermes/skills/` (Hermes Agent).
|
|
192
|
+
|
|
193
|
+
Design notes and the security model are in the [feature
|
|
194
|
+
documentation](https://github.com/neuromaxer/quaestor-lite/blob/main/docs/features/agent-connect/agent-connect.md).
|
|
195
|
+
|
|
196
|
+
## Requirements
|
|
197
|
+
|
|
198
|
+
Python 3.12+. Depends only on `httpx` and `typer` — it speaks HTTP and never imports the
|
|
199
|
+
Quaestor backend, so it installs in seconds and tolerates version skew against the server.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "quaestor-cli"
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
description = "Command-line client for the Quaestor API — built for humans and agents."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
# NOTE: no `license` yet — the repository has no LICENSE file, so publishing one here
|
|
8
|
+
# would be inventing a licensing decision. Until one is chosen, PyPI shows no license,
|
|
9
|
+
# which legally means all rights reserved.
|
|
10
|
+
keywords = ["quaestor", "habit-tracker", "cli", "agent", "ai"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 4 - Beta",
|
|
13
|
+
"Environment :: Console",
|
|
14
|
+
"Intended Audience :: End Users/Desktop",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Topic :: Utilities",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"httpx>=0.27",
|
|
23
|
+
# 0.16 is the first release that defaults pretty_exceptions_show_locals to False.
|
|
24
|
+
# Older versions render local variables — including the API key — into a crash
|
|
25
|
+
# traceback. main.py also sets the flag explicitly; this floor is belt and braces.
|
|
26
|
+
"typer>=0.16",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://quaestor.app"
|
|
31
|
+
Repository = "https://github.com/neuromaxer/quaestor-lite"
|
|
32
|
+
Documentation = "https://github.com/neuromaxer/quaestor-lite/blob/main/apps/cli/README.md"
|
|
33
|
+
Issues = "https://github.com/neuromaxer/quaestor-lite/issues"
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
qst = "quaestor_cli.main:app"
|
|
37
|
+
|
|
38
|
+
[dependency-groups]
|
|
39
|
+
dev = [
|
|
40
|
+
"pytest>=8.0",
|
|
41
|
+
"pytest-asyncio>=0.23",
|
|
42
|
+
"respx>=0.21",
|
|
43
|
+
"ruff>=0.6",
|
|
44
|
+
"mypy>=1.11",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[build-system]
|
|
48
|
+
requires = ["hatchling"]
|
|
49
|
+
build-backend = "hatchling.build"
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.wheel]
|
|
52
|
+
packages = ["src/quaestor_cli"]
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
testpaths = ["tests"]
|
|
56
|
+
asyncio_mode = "auto"
|
|
57
|
+
|
|
58
|
+
[tool.ruff]
|
|
59
|
+
line-length = 110
|
|
60
|
+
target-version = "py312"
|
|
61
|
+
src = ["src", "tests"]
|
|
62
|
+
|
|
63
|
+
[tool.ruff.lint]
|
|
64
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
65
|
+
|
|
66
|
+
[tool.mypy]
|
|
67
|
+
python_version = "3.12"
|
|
68
|
+
strict = true
|
|
69
|
+
warn_unreachable = true
|
|
@@ -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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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,
|
|
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
|
|
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
|
|
|
@@ -15,6 +15,26 @@ from quaestor_cli.runtime import CliState, JsonFlag, cli_state, execute
|
|
|
15
15
|
|
|
16
16
|
UNKNOWN = "unknown"
|
|
17
17
|
|
|
18
|
+
# Scopes a key needs for each command family. Keys made before Quests existed lack
|
|
19
|
+
# the quest scopes; new keys get them by default (both pre-checked in the dashboard).
|
|
20
|
+
FEATURE_SCOPES: dict[str, str] = {
|
|
21
|
+
"quests:read": "qst agenda, qst quests, qst quest show/log",
|
|
22
|
+
"quests:write": "qst quest add/plan/check/note/link/archive",
|
|
23
|
+
}
|
|
24
|
+
NEW_KEY_HINT = (
|
|
25
|
+
"Create a new key in the web app (Profile → Developer Access): "
|
|
26
|
+
"new keys include quests:read and quests:write by default."
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def scope_hints(scopes: list[str] | None) -> list[str]:
|
|
31
|
+
"""Explain which commands a key's missing scopes switch off, and how to fix it."""
|
|
32
|
+
if scopes is None:
|
|
33
|
+
return []
|
|
34
|
+
missing = [scope for scope in FEATURE_SCOPES if scope not in scopes]
|
|
35
|
+
hints = [f"{scope} not granted — {FEATURE_SCOPES[scope]} will fail (403)." for scope in missing]
|
|
36
|
+
return [*hints, NEW_KEY_HINT] if hints else []
|
|
37
|
+
|
|
18
38
|
|
|
19
39
|
@dataclass(frozen=True)
|
|
20
40
|
class Diagnosis:
|
|
@@ -30,6 +50,8 @@ class Diagnosis:
|
|
|
30
50
|
"""None when the server has no /auth/whoami, i.e. scopes are unknowable."""
|
|
31
51
|
user: str | None = None
|
|
32
52
|
detail: str | None = None
|
|
53
|
+
hints: tuple[str, ...] = ()
|
|
54
|
+
"""Advice that isn't a failure, e.g. scopes a key was created without."""
|
|
33
55
|
|
|
34
56
|
@property
|
|
35
57
|
def exit_code(self) -> ExitCode:
|
|
@@ -89,6 +111,7 @@ def diagnose(state: CliState) -> Diagnosis:
|
|
|
89
111
|
scopes=scopes,
|
|
90
112
|
user=user,
|
|
91
113
|
detail=detail,
|
|
114
|
+
hints=tuple(scope_hints(scopes)),
|
|
92
115
|
)
|
|
93
116
|
|
|
94
117
|
|
|
@@ -129,11 +152,14 @@ def _check_credential(state: CliState) -> _CredentialCheck:
|
|
|
129
152
|
|
|
130
153
|
def _lines(diagnosis: Diagnosis) -> tuple[str, ...]:
|
|
131
154
|
"""Render the diagnosis as aligned human-readable lines."""
|
|
132
|
-
scopes
|
|
133
|
-
format_scopes(diagnosis.scopes)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
155
|
+
if diagnosis.scopes is not None:
|
|
156
|
+
scopes = format_scopes(diagnosis.scopes)
|
|
157
|
+
elif not diagnosis.authenticated:
|
|
158
|
+
# The credential was rejected, so scopes were never knowable. Blaming the
|
|
159
|
+
# server's age here points the reader at the wrong problem.
|
|
160
|
+
scopes = f"{UNKNOWN} (credential was rejected)"
|
|
161
|
+
else:
|
|
162
|
+
scopes = f"{UNKNOWN} (this server has no /auth/whoami)"
|
|
137
163
|
fields = (
|
|
138
164
|
("cli version", diagnosis.cli_version),
|
|
139
165
|
("base url", diagnosis.base_url),
|
|
@@ -144,4 +170,6 @@ def _lines(diagnosis: Diagnosis) -> tuple[str, ...]:
|
|
|
144
170
|
("user", diagnosis.user or "-"),
|
|
145
171
|
("scopes", scopes),
|
|
146
172
|
)
|
|
147
|
-
|
|
173
|
+
lines = [f"{label + ':':<15}{value}" for label, value in fields]
|
|
174
|
+
lines += [f"{'hint:':<15}{hint}" for hint in diagnosis.hints]
|
|
175
|
+
return tuple(lines)
|