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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: doingapp
3
- Version: 0.5.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` for other dates) |
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` for other dates) |
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(email: str, password: str) -> dict:
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={"email": email, "password": password})
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
- """Create a new Doing account."""
134
- email = typer.prompt("Email")
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(email, password)
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, "email": email})
149
- console.print(f"[bold green]Account created![/bold green] Logged in as [bold]{email}[/bold]")
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
- task_id: int = typer.Argument(..., help="ID of the task to mark done"),
297
+ task_ids: list[int] = typer.Argument(..., help="ID(s) of the task(s) to mark done"),
272
298
  ):
273
- """Mark a task as done."""
274
- try:
275
- task = api.mark_done(task_id)
276
- except Exception as exc:
277
- _handle_api_error(exc)
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(task)
280
- console.print(f"[green]✓[/green] Done: [bold]{task['title']}[/bold]")
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
- _require_auth()
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.5.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` for other dates) |
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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "doingapp"
7
- version = "0.5.0"
7
+ version = "0.6.0"
8
8
  description = "A fast CLI task tracker — track what you're doing from the terminal."
9
9
  readme = "README.md"
10
10
  license = "MIT"
File without changes
File without changes
File without changes
File without changes