doingapp 0.5.0__tar.gz → 0.6.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.
- {doingapp-0.5.0 → doingapp-0.6.0}/PKG-INFO +32 -4
- {doingapp-0.5.0 → doingapp-0.6.0}/README.md +31 -3
- {doingapp-0.5.0 → doingapp-0.6.0}/doing_cli/api.py +57 -2
- {doingapp-0.5.0 → doingapp-0.6.0}/doing_cli/main.py +145 -16
- {doingapp-0.5.0 → doingapp-0.6.0}/doingapp.egg-info/PKG-INFO +32 -4
- {doingapp-0.5.0 → doingapp-0.6.0}/pyproject.toml +1 -1
- {doingapp-0.5.0 → doingapp-0.6.0}/LICENSE +0 -0
- {doingapp-0.5.0 → doingapp-0.6.0}/doing_cli/__init__.py +0 -0
- {doingapp-0.5.0 → doingapp-0.6.0}/doing_cli/auth.py +0 -0
- {doingapp-0.5.0 → doingapp-0.6.0}/doingapp.egg-info/SOURCES.txt +0 -0
- {doingapp-0.5.0 → doingapp-0.6.0}/doingapp.egg-info/dependency_links.txt +0 -0
- {doingapp-0.5.0 → doingapp-0.6.0}/doingapp.egg-info/entry_points.txt +0 -0
- {doingapp-0.5.0 → doingapp-0.6.0}/doingapp.egg-info/requires.txt +0 -0
- {doingapp-0.5.0 → doingapp-0.6.0}/doingapp.egg-info/top_level.txt +0 -0
- {doingapp-0.5.0 → doingapp-0.6.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: doingapp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: A fast CLI task tracker — track what you're doing from the terminal.
|
|
5
5
|
Author-email: Parker Grimes <parker@altellus.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -62,27 +62,55 @@ doing # See what's on your plate
|
|
|
62
62
|
doing done 1 # Mark task #1 as done
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
Or authenticate with an API key (get one from the web UI at Settings → API Keys):
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
doing apikey --key doing_xxxxx
|
|
69
|
+
```
|
|
70
|
+
|
|
65
71
|
## Commands
|
|
66
72
|
|
|
67
73
|
| Command | Description |
|
|
68
74
|
|---------|-------------|
|
|
69
75
|
| `doing` | Show active tasks (same as `doing ls`) |
|
|
70
76
|
| `doing add <title>` | Add a task (`-n` notes, `-c` context ID) |
|
|
77
|
+
| `doing get <id>` | Get a single task by ID |
|
|
71
78
|
| `doing done <id>` | Mark a task as done |
|
|
72
79
|
| `doing pause <id>` | Pause a task |
|
|
73
80
|
| `doing resume <id>` | Resume a paused task |
|
|
74
81
|
| `doing reopen <id>` | Reopen a completed task |
|
|
75
82
|
| `doing edit <id>` | Edit a task (`-t` title, `-n` notes, `-c` context, `--no-context`) |
|
|
76
83
|
| `doing rm <id>` | Delete a task |
|
|
77
|
-
| `doing ls` | List active tasks |
|
|
78
|
-
| `doing log` | Show tasks done today (`-d YYYY-MM-DD`
|
|
79
|
-
| `doing today` | Show all tasks grouped by status |
|
|
84
|
+
| `doing ls` | List active tasks (`-c` context ID) |
|
|
85
|
+
| `doing log` | Show tasks done today (`-d YYYY-MM-DD`, `-c` context ID) |
|
|
86
|
+
| `doing today` | Show all tasks grouped by status (`-c` context ID) |
|
|
80
87
|
| `doing clear` | Mark all active tasks as done |
|
|
81
88
|
| `doing contexts` | List contexts |
|
|
82
89
|
| `doing context add <name>` | Create a context |
|
|
83
90
|
| `doing context rm <id>` | Delete a context |
|
|
91
|
+
| `doing me` | Show current user info |
|
|
84
92
|
| `doing login` | Log in |
|
|
85
93
|
| `doing logout` | Log out |
|
|
94
|
+
| `doing apikey` | Save an API key (`-k` key) |
|
|
95
|
+
|
|
96
|
+
## JSON output
|
|
97
|
+
|
|
98
|
+
Every command supports `--json` (or `-j`) for machine-readable output. This is useful for scripting and AI coding agents like Claude Code or Codex CLI.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
doing --json ls # [{"id": 1, "title": "...", ...}]
|
|
102
|
+
doing --json add "New task" # {"id": 2, "title": "New task", ...}
|
|
103
|
+
doing --json done 2 # {"id": 2, "status": "done", ...}
|
|
104
|
+
doing --json contexts # [{"id": 1, "name": "Work", ...}]
|
|
105
|
+
doing --json get 2 # {"id": 2, "title": "...", ...}
|
|
106
|
+
doing --json me # {"id": 1, "email": "...", ...}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Errors also return structured JSON with a non-zero exit code:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
doing --json done 99999 # {"error": true, "status": 404, "detail": "Task not found"}
|
|
113
|
+
```
|
|
86
114
|
|
|
87
115
|
## Configuration
|
|
88
116
|
|
|
@@ -37,27 +37,55 @@ doing # See what's on your plate
|
|
|
37
37
|
doing done 1 # Mark task #1 as done
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
Or authenticate with an API key (get one from the web UI at Settings → API Keys):
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
doing apikey --key doing_xxxxx
|
|
44
|
+
```
|
|
45
|
+
|
|
40
46
|
## Commands
|
|
41
47
|
|
|
42
48
|
| Command | Description |
|
|
43
49
|
|---------|-------------|
|
|
44
50
|
| `doing` | Show active tasks (same as `doing ls`) |
|
|
45
51
|
| `doing add <title>` | Add a task (`-n` notes, `-c` context ID) |
|
|
52
|
+
| `doing get <id>` | Get a single task by ID |
|
|
46
53
|
| `doing done <id>` | Mark a task as done |
|
|
47
54
|
| `doing pause <id>` | Pause a task |
|
|
48
55
|
| `doing resume <id>` | Resume a paused task |
|
|
49
56
|
| `doing reopen <id>` | Reopen a completed task |
|
|
50
57
|
| `doing edit <id>` | Edit a task (`-t` title, `-n` notes, `-c` context, `--no-context`) |
|
|
51
58
|
| `doing rm <id>` | Delete a task |
|
|
52
|
-
| `doing ls` | List active tasks |
|
|
53
|
-
| `doing log` | Show tasks done today (`-d YYYY-MM-DD`
|
|
54
|
-
| `doing today` | Show all tasks grouped by status |
|
|
59
|
+
| `doing ls` | List active tasks (`-c` context ID) |
|
|
60
|
+
| `doing log` | Show tasks done today (`-d YYYY-MM-DD`, `-c` context ID) |
|
|
61
|
+
| `doing today` | Show all tasks grouped by status (`-c` context ID) |
|
|
55
62
|
| `doing clear` | Mark all active tasks as done |
|
|
56
63
|
| `doing contexts` | List contexts |
|
|
57
64
|
| `doing context add <name>` | Create a context |
|
|
58
65
|
| `doing context rm <id>` | Delete a context |
|
|
66
|
+
| `doing me` | Show current user info |
|
|
59
67
|
| `doing login` | Log in |
|
|
60
68
|
| `doing logout` | Log out |
|
|
69
|
+
| `doing apikey` | Save an API key (`-k` key) |
|
|
70
|
+
|
|
71
|
+
## JSON output
|
|
72
|
+
|
|
73
|
+
Every command supports `--json` (or `-j`) for machine-readable output. This is useful for scripting and AI coding agents like Claude Code or Codex CLI.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
doing --json ls # [{"id": 1, "title": "...", ...}]
|
|
77
|
+
doing --json add "New task" # {"id": 2, "title": "New task", ...}
|
|
78
|
+
doing --json done 2 # {"id": 2, "status": "done", ...}
|
|
79
|
+
doing --json contexts # [{"id": 1, "name": "Work", ...}]
|
|
80
|
+
doing --json get 2 # {"id": 2, "title": "...", ...}
|
|
81
|
+
doing --json me # {"id": 1, "email": "...", ...}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Errors also return structured JSON with a non-zero exit code:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
doing --json done 99999 # {"error": true, "status": 404, "detail": "Task not found"}
|
|
88
|
+
```
|
|
61
89
|
|
|
62
90
|
## Configuration
|
|
63
91
|
|
|
@@ -28,9 +28,9 @@ def _client() -> httpx.Client:
|
|
|
28
28
|
return httpx.Client(base_url=BASE_URL, timeout=10.0, headers=headers)
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
def register(
|
|
31
|
+
def register(token: str, password: str) -> dict:
|
|
32
32
|
with httpx.Client(base_url=BASE_URL, timeout=10.0) as c:
|
|
33
|
-
r = c.post("/auth/register", json={"
|
|
33
|
+
r = c.post("/auth/register", json={"token": token, "password": password})
|
|
34
34
|
r.raise_for_status()
|
|
35
35
|
return r.json()
|
|
36
36
|
|
|
@@ -134,6 +134,29 @@ def delete_task(task_id: int) -> None:
|
|
|
134
134
|
r.raise_for_status()
|
|
135
135
|
|
|
136
136
|
|
|
137
|
+
def get_stats(context_id: int | None = None) -> dict:
|
|
138
|
+
params: dict = {"tz": _local_tz()}
|
|
139
|
+
if context_id is not None:
|
|
140
|
+
params["context_id"] = context_id
|
|
141
|
+
with _client() as c:
|
|
142
|
+
r = c.get("/stats", params=params)
|
|
143
|
+
r.raise_for_status()
|
|
144
|
+
return r.json()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def get_done_history(limit: int = 7, before: str | None = None,
|
|
148
|
+
context_id: int | None = None) -> dict:
|
|
149
|
+
params: dict = {"tz": _local_tz(), "limit": limit}
|
|
150
|
+
if before is not None:
|
|
151
|
+
params["before"] = before
|
|
152
|
+
if context_id is not None:
|
|
153
|
+
params["context_id"] = context_id
|
|
154
|
+
with _client() as c:
|
|
155
|
+
r = c.get("/done", params=params)
|
|
156
|
+
r.raise_for_status()
|
|
157
|
+
return r.json()
|
|
158
|
+
|
|
159
|
+
|
|
137
160
|
def get_today(context_id: int | None = None) -> dict:
|
|
138
161
|
params: dict = {"tz": _local_tz()}
|
|
139
162
|
if context_id is not None:
|
|
@@ -161,6 +184,38 @@ def create_context(name: str) -> dict:
|
|
|
161
184
|
return r.json()
|
|
162
185
|
|
|
163
186
|
|
|
187
|
+
def get_context(context_id: int) -> dict:
|
|
188
|
+
with _client() as c:
|
|
189
|
+
r = c.get(f"/contexts/{context_id}")
|
|
190
|
+
r.raise_for_status()
|
|
191
|
+
return r.json()
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def update_context(context_id: int, name: str, icon: str | None = None) -> dict:
|
|
195
|
+
payload: dict = {"name": name}
|
|
196
|
+
if icon is not None:
|
|
197
|
+
payload["icon"] = icon
|
|
198
|
+
with _client() as c:
|
|
199
|
+
r = c.put(f"/contexts/{context_id}", json=payload)
|
|
200
|
+
r.raise_for_status()
|
|
201
|
+
return r.json()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def snooze_context(context_id: int) -> dict:
|
|
205
|
+
params: dict = {"tz": _local_tz()}
|
|
206
|
+
with _client() as c:
|
|
207
|
+
r = c.post(f"/contexts/{context_id}/snooze", params=params)
|
|
208
|
+
r.raise_for_status()
|
|
209
|
+
return r.json()
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def unsnooze_context(context_id: int) -> dict:
|
|
213
|
+
with _client() as c:
|
|
214
|
+
r = c.post(f"/contexts/{context_id}/unsnooze")
|
|
215
|
+
r.raise_for_status()
|
|
216
|
+
return r.json()
|
|
217
|
+
|
|
218
|
+
|
|
164
219
|
def delete_context(context_id: int) -> None:
|
|
165
220
|
with _client() as c:
|
|
166
221
|
r = c.delete(f"/contexts/{context_id}")
|
|
@@ -75,6 +75,30 @@ def _handle_api_error(exc: Exception):
|
|
|
75
75
|
raise typer.Exit(code=1)
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
def _validation_error(detail: str):
|
|
79
|
+
"""Print a validation error (JSON or Rich) and exit with code 1."""
|
|
80
|
+
if _json_mode:
|
|
81
|
+
_output_json({"error": True, "detail": detail})
|
|
82
|
+
console.print(f"[bold red]{detail}[/bold red]")
|
|
83
|
+
raise typer.Exit(code=1)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _validate_title(title: str) -> str:
|
|
87
|
+
"""Strip whitespace and newlines from title, validate non-empty."""
|
|
88
|
+
cleaned = title.replace("\n", " ").replace("\r", " ").strip()
|
|
89
|
+
if not cleaned:
|
|
90
|
+
_validation_error("Title cannot be empty")
|
|
91
|
+
return cleaned
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _validate_name(name: str) -> str:
|
|
95
|
+
"""Strip and validate non-empty name for contexts."""
|
|
96
|
+
cleaned = name.strip()
|
|
97
|
+
if not cleaned:
|
|
98
|
+
_validation_error("Name cannot be empty")
|
|
99
|
+
return cleaned
|
|
100
|
+
|
|
101
|
+
|
|
78
102
|
def _print_notes(task: dict) -> None:
|
|
79
103
|
if task.get("notes"):
|
|
80
104
|
console.print(f" [dim]{task['notes']}[/dim]")
|
|
@@ -129,9 +153,10 @@ def login():
|
|
|
129
153
|
|
|
130
154
|
|
|
131
155
|
@app.command()
|
|
132
|
-
def register(
|
|
133
|
-
"
|
|
134
|
-
|
|
156
|
+
def register(
|
|
157
|
+
token: str = typer.Argument(..., help="Invite token from your invitation link"),
|
|
158
|
+
):
|
|
159
|
+
"""Create a new Doing account using an invite token."""
|
|
135
160
|
password = typer.prompt("Password", hide_input=True)
|
|
136
161
|
password_confirm = typer.prompt("Confirm password", hide_input=True)
|
|
137
162
|
if password != password_confirm:
|
|
@@ -140,13 +165,13 @@ def register():
|
|
|
140
165
|
console.print("[bold red]Passwords do not match.[/bold red]")
|
|
141
166
|
raise typer.Exit(code=1)
|
|
142
167
|
try:
|
|
143
|
-
data = api.register(
|
|
168
|
+
data = api.register(token, password)
|
|
144
169
|
except Exception as exc:
|
|
145
170
|
_handle_api_error(exc)
|
|
146
171
|
save_token(data["access_token"])
|
|
147
172
|
if _json_mode:
|
|
148
|
-
_output_json({"ok": True
|
|
149
|
-
console.print(
|
|
173
|
+
_output_json({"ok": True})
|
|
174
|
+
console.print("[bold green]Account created![/bold green] You're now logged in.")
|
|
150
175
|
|
|
151
176
|
|
|
152
177
|
@app.command()
|
|
@@ -197,6 +222,7 @@ def add(
|
|
|
197
222
|
context: int = typer.Option(None, "--context", "-c", help="Context ID"),
|
|
198
223
|
):
|
|
199
224
|
"""Add something to your plate."""
|
|
225
|
+
title = _validate_title(title)
|
|
200
226
|
try:
|
|
201
227
|
task = api.create_task(title, notes, context)
|
|
202
228
|
except Exception as exc:
|
|
@@ -268,16 +294,28 @@ def log(
|
|
|
268
294
|
|
|
269
295
|
@app.command()
|
|
270
296
|
def done(
|
|
271
|
-
|
|
297
|
+
task_ids: list[int] = typer.Argument(..., help="ID(s) of the task(s) to mark done"),
|
|
272
298
|
):
|
|
273
|
-
"""Mark
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
299
|
+
"""Mark one or more tasks as done."""
|
|
300
|
+
results = []
|
|
301
|
+
errors = []
|
|
302
|
+
for task_id in task_ids:
|
|
303
|
+
try:
|
|
304
|
+
task = api.mark_done(task_id)
|
|
305
|
+
except Exception as exc:
|
|
306
|
+
if _json_mode and len(task_ids) == 1:
|
|
307
|
+
_handle_api_error(exc)
|
|
308
|
+
if not _json_mode:
|
|
309
|
+
console.print(f"[red]Failed to complete task #{task_id}[/red]")
|
|
310
|
+
errors.append(task_id)
|
|
311
|
+
continue
|
|
312
|
+
results.append(task)
|
|
313
|
+
if not _json_mode:
|
|
314
|
+
console.print(f"[green]✓[/green] Done: [bold]{task['title']}[/bold]")
|
|
278
315
|
if _json_mode:
|
|
279
|
-
_output_json(
|
|
280
|
-
|
|
316
|
+
_output_json(results[0] if len(task_ids) == 1 and results else results)
|
|
317
|
+
if errors:
|
|
318
|
+
raise typer.Exit(code=1)
|
|
281
319
|
|
|
282
320
|
|
|
283
321
|
@app.command()
|
|
@@ -331,6 +369,15 @@ def edit(
|
|
|
331
369
|
no_context: bool = typer.Option(False, "--no-context", help="Remove context"),
|
|
332
370
|
):
|
|
333
371
|
"""Edit a task's title, notes, or context."""
|
|
372
|
+
if no_context and context is not None:
|
|
373
|
+
_validation_error("--no-context and --context cannot be used together")
|
|
374
|
+
|
|
375
|
+
if title is None and notes is None and context is None and not no_context:
|
|
376
|
+
_validation_error("No changes specified. Use --title, --notes, --context, or --no-context.")
|
|
377
|
+
|
|
378
|
+
if title is not None:
|
|
379
|
+
title = _validate_title(title)
|
|
380
|
+
|
|
334
381
|
try:
|
|
335
382
|
task = api.update_task(task_id, title=title, notes=notes,
|
|
336
383
|
context_id=context, clear_context=no_context)
|
|
@@ -447,7 +494,7 @@ def context_add(
|
|
|
447
494
|
name: str = typer.Argument(..., help="Context name"),
|
|
448
495
|
):
|
|
449
496
|
"""Create a new context."""
|
|
450
|
-
|
|
497
|
+
name = _validate_name(name)
|
|
451
498
|
try:
|
|
452
499
|
ctx = api.create_context(name)
|
|
453
500
|
except Exception as exc:
|
|
@@ -457,12 +504,69 @@ def context_add(
|
|
|
457
504
|
console.print(f"[green bold]Created context:[/green bold] {ctx['name']} [dim](#{ctx['id']})[/dim]")
|
|
458
505
|
|
|
459
506
|
|
|
507
|
+
@context_app.command("edit")
|
|
508
|
+
def context_edit(
|
|
509
|
+
context_id: int = typer.Argument(..., help="Context ID to edit"),
|
|
510
|
+
name: str = typer.Option(None, "--name", "-n", help="New name"),
|
|
511
|
+
icon: str = typer.Option(None, "--icon", "-i", help="New icon (emoji)"),
|
|
512
|
+
):
|
|
513
|
+
"""Rename a context or change its icon."""
|
|
514
|
+
if name is None and icon is None:
|
|
515
|
+
_validation_error("No changes specified. Use --name or --icon.")
|
|
516
|
+
|
|
517
|
+
if name is not None:
|
|
518
|
+
name = _validate_name(name)
|
|
519
|
+
|
|
520
|
+
# Need current name if only changing icon (API requires name)
|
|
521
|
+
if name is None:
|
|
522
|
+
try:
|
|
523
|
+
current = api.get_context(context_id)
|
|
524
|
+
except Exception as exc:
|
|
525
|
+
_handle_api_error(exc)
|
|
526
|
+
name = current["name"]
|
|
527
|
+
|
|
528
|
+
try:
|
|
529
|
+
ctx = api.update_context(context_id, name=name, icon=icon)
|
|
530
|
+
except Exception as exc:
|
|
531
|
+
_handle_api_error(exc)
|
|
532
|
+
if _json_mode:
|
|
533
|
+
_output_json(ctx)
|
|
534
|
+
console.print(f"[green bold]Updated context:[/green bold] {ctx['name']} [dim](#{ctx['id']})[/dim]")
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
@context_app.command("snooze")
|
|
538
|
+
def context_snooze(
|
|
539
|
+
context_id: int = typer.Argument(..., help="Context ID to snooze"),
|
|
540
|
+
):
|
|
541
|
+
"""Snooze a context until end of day (hides its tasks)."""
|
|
542
|
+
try:
|
|
543
|
+
ctx = api.snooze_context(context_id)
|
|
544
|
+
except Exception as exc:
|
|
545
|
+
_handle_api_error(exc)
|
|
546
|
+
if _json_mode:
|
|
547
|
+
_output_json(ctx)
|
|
548
|
+
console.print(f"[yellow]😴 Snoozed:[/yellow] {ctx['name']} until end of day")
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
@context_app.command("unsnooze")
|
|
552
|
+
def context_unsnooze(
|
|
553
|
+
context_id: int = typer.Argument(..., help="Context ID to unsnooze"),
|
|
554
|
+
):
|
|
555
|
+
"""Unsnooze a context (show its tasks again)."""
|
|
556
|
+
try:
|
|
557
|
+
ctx = api.unsnooze_context(context_id)
|
|
558
|
+
except Exception as exc:
|
|
559
|
+
_handle_api_error(exc)
|
|
560
|
+
if _json_mode:
|
|
561
|
+
_output_json(ctx)
|
|
562
|
+
console.print(f"[green bold]Unsnoozed:[/green bold] {ctx['name']}")
|
|
563
|
+
|
|
564
|
+
|
|
460
565
|
@context_app.command("rm")
|
|
461
566
|
def context_rm(
|
|
462
567
|
context_id: int = typer.Argument(..., help="Context ID to delete"),
|
|
463
568
|
):
|
|
464
569
|
"""Delete a context (tasks keep their data, just lose the context)."""
|
|
465
|
-
_require_auth()
|
|
466
570
|
try:
|
|
467
571
|
api.delete_context(context_id)
|
|
468
572
|
except Exception as exc:
|
|
@@ -472,5 +576,30 @@ def context_rm(
|
|
|
472
576
|
console.print(f"[dim]Deleted context #{context_id}.[/dim]")
|
|
473
577
|
|
|
474
578
|
|
|
579
|
+
@app.command()
|
|
580
|
+
def stats(
|
|
581
|
+
context: int = typer.Option(None, "--context", "-c", help="Filter by context ID"),
|
|
582
|
+
):
|
|
583
|
+
"""Show how many tasks you've completed today and this week."""
|
|
584
|
+
try:
|
|
585
|
+
data = api.get_stats(context_id=context)
|
|
586
|
+
except Exception as exc:
|
|
587
|
+
_handle_api_error(exc)
|
|
588
|
+
|
|
589
|
+
if _json_mode:
|
|
590
|
+
_output_json(data)
|
|
591
|
+
|
|
592
|
+
today_count = data["today"]
|
|
593
|
+
week_count = data["this_week"]
|
|
594
|
+
total_count = data["total"]
|
|
595
|
+
|
|
596
|
+
console.print(f"\n [bold]Today:[/bold] {today_count} task{'s' if today_count != 1 else ''} done")
|
|
597
|
+
console.print(f" [bold]This week:[/bold] {week_count} task{'s' if week_count != 1 else ''} done")
|
|
598
|
+
|
|
599
|
+
if total_count > week_count:
|
|
600
|
+
console.print(f" [dim]All time: {total_count} total[/dim]")
|
|
601
|
+
console.print()
|
|
602
|
+
|
|
603
|
+
|
|
475
604
|
if __name__ == "__main__":
|
|
476
605
|
app()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: doingapp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: A fast CLI task tracker — track what you're doing from the terminal.
|
|
5
5
|
Author-email: Parker Grimes <parker@altellus.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -62,27 +62,55 @@ doing # See what's on your plate
|
|
|
62
62
|
doing done 1 # Mark task #1 as done
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
Or authenticate with an API key (get one from the web UI at Settings → API Keys):
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
doing apikey --key doing_xxxxx
|
|
69
|
+
```
|
|
70
|
+
|
|
65
71
|
## Commands
|
|
66
72
|
|
|
67
73
|
| Command | Description |
|
|
68
74
|
|---------|-------------|
|
|
69
75
|
| `doing` | Show active tasks (same as `doing ls`) |
|
|
70
76
|
| `doing add <title>` | Add a task (`-n` notes, `-c` context ID) |
|
|
77
|
+
| `doing get <id>` | Get a single task by ID |
|
|
71
78
|
| `doing done <id>` | Mark a task as done |
|
|
72
79
|
| `doing pause <id>` | Pause a task |
|
|
73
80
|
| `doing resume <id>` | Resume a paused task |
|
|
74
81
|
| `doing reopen <id>` | Reopen a completed task |
|
|
75
82
|
| `doing edit <id>` | Edit a task (`-t` title, `-n` notes, `-c` context, `--no-context`) |
|
|
76
83
|
| `doing rm <id>` | Delete a task |
|
|
77
|
-
| `doing ls` | List active tasks |
|
|
78
|
-
| `doing log` | Show tasks done today (`-d YYYY-MM-DD`
|
|
79
|
-
| `doing today` | Show all tasks grouped by status |
|
|
84
|
+
| `doing ls` | List active tasks (`-c` context ID) |
|
|
85
|
+
| `doing log` | Show tasks done today (`-d YYYY-MM-DD`, `-c` context ID) |
|
|
86
|
+
| `doing today` | Show all tasks grouped by status (`-c` context ID) |
|
|
80
87
|
| `doing clear` | Mark all active tasks as done |
|
|
81
88
|
| `doing contexts` | List contexts |
|
|
82
89
|
| `doing context add <name>` | Create a context |
|
|
83
90
|
| `doing context rm <id>` | Delete a context |
|
|
91
|
+
| `doing me` | Show current user info |
|
|
84
92
|
| `doing login` | Log in |
|
|
85
93
|
| `doing logout` | Log out |
|
|
94
|
+
| `doing apikey` | Save an API key (`-k` key) |
|
|
95
|
+
|
|
96
|
+
## JSON output
|
|
97
|
+
|
|
98
|
+
Every command supports `--json` (or `-j`) for machine-readable output. This is useful for scripting and AI coding agents like Claude Code or Codex CLI.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
doing --json ls # [{"id": 1, "title": "...", ...}]
|
|
102
|
+
doing --json add "New task" # {"id": 2, "title": "New task", ...}
|
|
103
|
+
doing --json done 2 # {"id": 2, "status": "done", ...}
|
|
104
|
+
doing --json contexts # [{"id": 1, "name": "Work", ...}]
|
|
105
|
+
doing --json get 2 # {"id": 2, "title": "...", ...}
|
|
106
|
+
doing --json me # {"id": 1, "email": "...", ...}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Errors also return structured JSON with a non-zero exit code:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
doing --json done 99999 # {"error": true, "status": 404, "detail": "Task not found"}
|
|
113
|
+
```
|
|
86
114
|
|
|
87
115
|
## Configuration
|
|
88
116
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|