doingapp 0.6.0__tar.gz → 0.7.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: doingapp
3
- Version: 0.6.0
3
+ Version: 0.7.1
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
@@ -73,13 +73,14 @@ doing apikey --key doing_xxxxx
73
73
  | Command | Description |
74
74
  |---------|-------------|
75
75
  | `doing` | Show active tasks (same as `doing ls`) |
76
- | `doing add <title>` | Add a task (`-n` notes, `-c` context ID) |
76
+ | `doing add <title>` | Add a task (`-n` notes, `-c` context ID, `-d "tomorrow 3pm"` due, `-r daily` repeat) |
77
77
  | `doing get <id>` | Get a single task by ID |
78
78
  | `doing done <id>` | Mark a task as done |
79
79
  | `doing pause <id>` | Pause a task |
80
80
  | `doing resume <id>` | Resume a paused task |
81
81
  | `doing reopen <id>` | Reopen a completed task |
82
- | `doing edit <id>` | Edit a task (`-t` title, `-n` notes, `-c` context, `--no-context`) |
82
+ | `doing edit <id>` | Edit a task (`-t` title, `-n` notes, `-c` context, `--no-context`, `-d` due, `--no-due`, `-r` repeat, `--no-repeat`) |
83
+ | `doing snooze <id> [10m\|1h\|3h\|1d\|morning]` | Push a due task back (default 10m) |
83
84
  | `doing rm <id>` | Delete a task |
84
85
  | `doing ls` | List active tasks (`-c` context ID) |
85
86
  | `doing log` | Show tasks done today (`-d YYYY-MM-DD`, `-c` context ID) |
@@ -93,6 +94,19 @@ doing apikey --key doing_xxxxx
93
94
  | `doing logout` | Log out |
94
95
  | `doing apikey` | Save an API key (`-k` key) |
95
96
 
97
+ ## Due dates and reminders
98
+
99
+ Give a task a due date and the Doing iOS app nags you every few minutes once it's overdue, until you mark it done or snooze it.
100
+
101
+ ```bash
102
+ doing add "Call mom" -d "tomorrow 3pm"
103
+ doing add "Water plants" -d friday -r 3d # repeats 3 days after each completion
104
+ doing add "Standup notes" -d "9am" -r weekdays
105
+ doing snooze 12 1h
106
+ ```
107
+
108
+ `-d` accepts natural language: `in 20m`, `tonight`, `fri 9am`, `oct 30`. Dates without a time use your morning time (9:00 by default). `-r` accepts `daily`, `weekdays`, days like `mon,thu`, or `Nd` (N days after done).
109
+
96
110
  ## JSON output
97
111
 
98
112
  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.
@@ -48,13 +48,14 @@ doing apikey --key doing_xxxxx
48
48
  | Command | Description |
49
49
  |---------|-------------|
50
50
  | `doing` | Show active tasks (same as `doing ls`) |
51
- | `doing add <title>` | Add a task (`-n` notes, `-c` context ID) |
51
+ | `doing add <title>` | Add a task (`-n` notes, `-c` context ID, `-d "tomorrow 3pm"` due, `-r daily` repeat) |
52
52
  | `doing get <id>` | Get a single task by ID |
53
53
  | `doing done <id>` | Mark a task as done |
54
54
  | `doing pause <id>` | Pause a task |
55
55
  | `doing resume <id>` | Resume a paused task |
56
56
  | `doing reopen <id>` | Reopen a completed task |
57
- | `doing edit <id>` | Edit a task (`-t` title, `-n` notes, `-c` context, `--no-context`) |
57
+ | `doing edit <id>` | Edit a task (`-t` title, `-n` notes, `-c` context, `--no-context`, `-d` due, `--no-due`, `-r` repeat, `--no-repeat`) |
58
+ | `doing snooze <id> [10m\|1h\|3h\|1d\|morning]` | Push a due task back (default 10m) |
58
59
  | `doing rm <id>` | Delete a task |
59
60
  | `doing ls` | List active tasks (`-c` context ID) |
60
61
  | `doing log` | Show tasks done today (`-d YYYY-MM-DD`, `-c` context ID) |
@@ -68,6 +69,19 @@ doing apikey --key doing_xxxxx
68
69
  | `doing logout` | Log out |
69
70
  | `doing apikey` | Save an API key (`-k` key) |
70
71
 
72
+ ## Due dates and reminders
73
+
74
+ Give a task a due date and the Doing iOS app nags you every few minutes once it's overdue, until you mark it done or snooze it.
75
+
76
+ ```bash
77
+ doing add "Call mom" -d "tomorrow 3pm"
78
+ doing add "Water plants" -d friday -r 3d # repeats 3 days after each completion
79
+ doing add "Standup notes" -d "9am" -r weekdays
80
+ doing snooze 12 1h
81
+ ```
82
+
83
+ `-d` accepts natural language: `in 20m`, `tonight`, `fri 9am`, `oct 30`. Dates without a time use your morning time (9:00 by default). `-r` accepts `daily`, `weekdays`, days like `mon,thu`, or `Nd` (N days after done).
84
+
71
85
  ## JSON output
72
86
 
73
87
  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.
@@ -1,5 +1,5 @@
1
1
  import os
2
- import time as _time
2
+ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
3
3
 
4
4
  import httpx
5
5
 
@@ -8,16 +8,35 @@ from doing_cli.auth import load_token
8
8
  BASE_URL = os.environ.get("DOING_API_URL", "https://doingapp.co")
9
9
 
10
10
 
11
- def _local_tz() -> str:
11
+ def _is_iana_tz(name: str) -> bool:
12
12
  try:
13
- # Works on most Unix systems
14
- return os.path.realpath("/etc/localtime").split("zoneinfo/")[1]
13
+ ZoneInfo(name)
14
+ except (ZoneInfoNotFoundError, ValueError):
15
+ return False
16
+ return True
17
+
18
+
19
+ def _local_tz() -> str | None:
20
+ """IANA name of the local timezone, or None if it can't be determined.
21
+
22
+ TZ wins over /etc/localtime, as it does for the OS itself; containers
23
+ commonly set TZ while /etc/localtime still points at UTC. Abbreviations
24
+ like "MST" are never sent: the server would treat them as fixed offsets
25
+ with no DST. When this returns None the server uses the account timezone.
26
+ """
27
+ candidates = []
28
+ if tz := os.environ.get("TZ", "").lstrip(":"):
29
+ candidates.append(tz)
30
+ try:
31
+ candidates.append(os.path.realpath("/etc/localtime").split("zoneinfo/")[1])
15
32
  except (IndexError, OSError):
16
33
  pass
17
- tz = os.environ.get("TZ")
18
- if tz:
19
- return tz
20
- return _time.tzname[0]
34
+ return next((name for name in candidates if _is_iana_tz(name)), None)
35
+
36
+
37
+ def _tz_params() -> dict:
38
+ tz = _local_tz()
39
+ return {"tz": tz} if tz else {}
21
40
 
22
41
 
23
42
  def _client() -> httpx.Client:
@@ -49,14 +68,19 @@ def get_me() -> dict:
49
68
  return r.json()
50
69
 
51
70
 
52
- def create_task(title: str, notes: str | None = None, context_id: int | None = None) -> dict:
71
+ def create_task(title: str, notes: str | None = None, context_id: int | None = None,
72
+ due: str | None = None, recurrence: str | None = None) -> dict:
53
73
  payload: dict = {"title": title}
54
74
  if notes is not None:
55
75
  payload["notes"] = notes
56
76
  if context_id is not None:
57
77
  payload["context_id"] = context_id
78
+ if due is not None:
79
+ payload["due"] = due
80
+ if recurrence is not None:
81
+ payload["recurrence"] = recurrence
58
82
  with _client() as c:
59
- r = c.post("/tasks", json=payload)
83
+ r = c.post("/tasks", json=payload, params=_tz_params())
60
84
  r.raise_for_status()
61
85
  return r.json()
62
86
 
@@ -70,7 +94,7 @@ def get_task(task_id: int) -> dict:
70
94
 
71
95
  def list_tasks(status: str | None = None, date: str | None = None,
72
96
  context_id: int | None = None) -> list[dict]:
73
- params: dict = {"tz": _local_tz()}
97
+ params: dict = _tz_params()
74
98
  if status is not None:
75
99
  params["status"] = status
76
100
  if date is not None:
@@ -85,7 +109,7 @@ def list_tasks(status: str | None = None, date: str | None = None,
85
109
 
86
110
  def mark_done(task_id: int) -> dict:
87
111
  with _client() as c:
88
- r = c.post(f"/tasks/{task_id}/done")
112
+ r = c.post(f"/tasks/{task_id}/done", params=_tz_params())
89
113
  r.raise_for_status()
90
114
  return r.json()
91
115
 
@@ -112,7 +136,9 @@ def resume_task(task_id: int) -> dict:
112
136
 
113
137
 
114
138
  def update_task(task_id: int, title: str | None = None, notes: str | None = None,
115
- context_id: int | None = None, clear_context: bool = False) -> dict:
139
+ context_id: int | None = None, clear_context: bool = False,
140
+ due: str | None = None, recurrence: str | None = None,
141
+ clear_due: bool = False, clear_recurrence: bool = False) -> dict:
116
142
  payload: dict = {}
117
143
  if title is not None:
118
144
  payload["title"] = title
@@ -122,8 +148,24 @@ def update_task(task_id: int, title: str | None = None, notes: str | None = None
122
148
  payload["clear_context"] = True
123
149
  elif context_id is not None:
124
150
  payload["context_id"] = context_id
151
+ if clear_due:
152
+ payload["clear_due"] = True
153
+ elif due is not None:
154
+ payload["due"] = due
155
+ if clear_recurrence:
156
+ payload["clear_recurrence"] = True
157
+ elif recurrence is not None:
158
+ payload["recurrence"] = recurrence
159
+ with _client() as c:
160
+ r = c.patch(f"/tasks/{task_id}", json=payload, params=_tz_params())
161
+ r.raise_for_status()
162
+ return r.json()
163
+
164
+
165
+ def snooze_task(task_id: int, minutes: int | None = None, preset: str | None = None) -> dict:
166
+ payload: dict = {"minutes": minutes} if minutes is not None else {"preset": preset}
125
167
  with _client() as c:
126
- r = c.patch(f"/tasks/{task_id}", json=payload)
168
+ r = c.post(f"/tasks/{task_id}/snooze", json=payload, params=_tz_params())
127
169
  r.raise_for_status()
128
170
  return r.json()
129
171
 
@@ -135,7 +177,7 @@ def delete_task(task_id: int) -> None:
135
177
 
136
178
 
137
179
  def get_stats(context_id: int | None = None) -> dict:
138
- params: dict = {"tz": _local_tz()}
180
+ params: dict = _tz_params()
139
181
  if context_id is not None:
140
182
  params["context_id"] = context_id
141
183
  with _client() as c:
@@ -146,7 +188,7 @@ def get_stats(context_id: int | None = None) -> dict:
146
188
 
147
189
  def get_done_history(limit: int = 7, before: str | None = None,
148
190
  context_id: int | None = None) -> dict:
149
- params: dict = {"tz": _local_tz(), "limit": limit}
191
+ params: dict = {**_tz_params(), "limit": limit}
150
192
  if before is not None:
151
193
  params["before"] = before
152
194
  if context_id is not None:
@@ -158,7 +200,7 @@ def get_done_history(limit: int = 7, before: str | None = None,
158
200
 
159
201
 
160
202
  def get_today(context_id: int | None = None) -> dict:
161
- params: dict = {"tz": _local_tz()}
203
+ params: dict = _tz_params()
162
204
  if context_id is not None:
163
205
  params["context_id"] = context_id
164
206
  with _client() as c:
@@ -202,7 +244,7 @@ def update_context(context_id: int, name: str, icon: str | None = None) -> dict:
202
244
 
203
245
 
204
246
  def snooze_context(context_id: int) -> dict:
205
- params: dict = {"tz": _local_tz()}
247
+ params: dict = _tz_params()
206
248
  with _client() as c:
207
249
  r = c.post(f"/contexts/{context_id}/snooze", params=params)
208
250
  r.raise_for_status()
@@ -1,5 +1,7 @@
1
1
  import json
2
+ import re
2
3
  import sys
4
+ from datetime import datetime, timezone
3
5
 
4
6
  import httpx
5
7
  import typer
@@ -99,6 +101,51 @@ def _validate_name(name: str) -> str:
99
101
  return cleaned
100
102
 
101
103
 
104
+ WEEKDAYS = ("mon", "tue", "wed", "thu", "fri", "sat", "sun")
105
+
106
+
107
+ def _normalize_repeat(value: str) -> str:
108
+ """Accept friendly repeat specs: daily, weekdays, mon,thu, 3d, or the raw API form."""
109
+ v = value.strip().lower().replace(" ", "")
110
+ if v in ("daily", "weekdays") or v.startswith(("weekly:", "after:")):
111
+ return v
112
+ if all(day in WEEKDAYS for day in v.split(",")):
113
+ return f"weekly:{v}"
114
+ if m := re.fullmatch(r"(\d+)d(ays?)?", v):
115
+ return f"after:{m.group(1)}"
116
+ _validation_error(f"Invalid --repeat '{value}'. Use daily, weekdays, mon,thu, or 3d (days after done).")
117
+
118
+
119
+ def _describe_repeat(rule: str) -> str:
120
+ if rule.startswith("weekly:"):
121
+ return "every " + ", ".join(d.title() for d in rule.split(":", 1)[1].split(","))
122
+ if rule.startswith("after:"):
123
+ n = rule.split(":", 1)[1]
124
+ return f"{n} day{'s' if n != '1' else ''} after done"
125
+ return rule
126
+
127
+
128
+ def _format_due(task: dict) -> str:
129
+ """Rich-formatted due label, e.g. '[red]overdue · Mon 3:00 PM[/red]'."""
130
+ if not task.get("due_at"):
131
+ return ""
132
+ due = datetime.fromisoformat(task["due_at"])
133
+ if due.tzinfo is None:
134
+ due = due.replace(tzinfo=timezone.utc)
135
+ due = due.astimezone()
136
+ now = datetime.now().astimezone()
137
+ if task.get("due_all_day"):
138
+ when = due.strftime("%a %b %-d")
139
+ elif due.date() == now.date():
140
+ when = due.strftime("%-I:%M %p")
141
+ else:
142
+ when = due.strftime("%a %b %-d %-I:%M %p")
143
+ repeat = f" ↻ {_describe_repeat(task['recurrence'])}" if task.get("recurrence") else ""
144
+ if task.get("status") == "active" and due <= now:
145
+ return f" [bold red]⏰ overdue · {when}{repeat}[/bold red]"
146
+ return f" [cyan]⏰ {when}{repeat}[/cyan]"
147
+
148
+
102
149
  def _print_notes(task: dict) -> None:
103
150
  if task.get("notes"):
104
151
  console.print(f" [dim]{task['notes']}[/dim]")
@@ -106,13 +153,13 @@ def _print_notes(task: dict) -> None:
106
153
 
107
154
  def _print_active_task(task: dict) -> None:
108
155
  ctx = f" [dim][{task['context_name']}][/dim]" if task.get("context_name") else ""
109
- console.print(f" [green bold]●[/green bold] [dim]#{task['id']}[/dim] {task['title']}{ctx}")
156
+ console.print(f" [green bold]●[/green bold] [dim]#{task['id']}[/dim] {task['title']}{ctx}{_format_due(task)}")
110
157
  _print_notes(task)
111
158
 
112
159
 
113
160
  def _print_paused_task(task: dict) -> None:
114
161
  ctx = f" [dim][{task['context_name']}][/dim]" if task.get("context_name") else ""
115
- console.print(f" [yellow]⏸[/yellow] [dim]#{task['id']}[/dim] {task['title']}{ctx}")
162
+ console.print(f" [yellow]⏸[/yellow] [dim]#{task['id']}[/dim] {task['title']}{ctx}{_format_due(task)}")
116
163
  _print_notes(task)
117
164
 
118
165
 
@@ -220,16 +267,19 @@ def add(
220
267
  title: str = typer.Argument(..., help="What are you working on?"),
221
268
  notes: str = typer.Option(None, "--notes", "-n", help="Optional notes"),
222
269
  context: int = typer.Option(None, "--context", "-c", help="Context ID"),
270
+ due: str = typer.Option(None, "--due", "-d", help="When it's due, e.g. 'tomorrow 3pm', 'in 20m', 'friday'"),
271
+ repeat: str = typer.Option(None, "--repeat", "-r", help="daily, weekdays, mon,thu, or 3d (3 days after done)"),
223
272
  ):
224
273
  """Add something to your plate."""
225
274
  title = _validate_title(title)
275
+ recurrence = _normalize_repeat(repeat) if repeat else None
226
276
  try:
227
- task = api.create_task(title, notes, context)
277
+ task = api.create_task(title, notes, context, due=due, recurrence=recurrence)
228
278
  except Exception as exc:
229
279
  _handle_api_error(exc)
230
280
  if _json_mode:
231
281
  _output_json(task)
232
- console.print(f"[green bold]●[/green bold] Added: [bold]{task['title']}[/bold] [dim](#{task['id']})[/dim]")
282
+ console.print(f"[green bold]●[/green bold] Added: [bold]{task['title']}[/bold] [dim](#{task['id']})[/dim]{_format_due(task)}")
233
283
 
234
284
 
235
285
  @app.command()
@@ -246,7 +296,7 @@ def get(
246
296
  status_icon = {"active": "[green bold]●[/green bold]", "paused": "[yellow]⏸[/yellow]", "done": "[green]✓[/green]"}
247
297
  icon = status_icon.get(task["status"], "?")
248
298
  ctx = f" [dim][{task['context_name']}][/dim]" if task.get("context_name") else ""
249
- console.print(f" {icon} [dim]#{task['id']}[/dim] {task['title']}{ctx}")
299
+ console.print(f" {icon} [dim]#{task['id']}[/dim] {task['title']}{ctx}{_format_due(task)}")
250
300
  if task.get("notes"):
251
301
  console.print(f" [dim]{task['notes']}[/dim]")
252
302
  console.print(f" [dim]Status: {task['status']} Created: {task['created_at']}[/dim]")
@@ -311,7 +361,8 @@ def done(
311
361
  continue
312
362
  results.append(task)
313
363
  if not _json_mode:
314
- console.print(f"[green]✓[/green] Done: [bold]{task['title']}[/bold]")
364
+ next_due = f" [dim]next:[/dim]{_format_due(task)}" if task.get("recurrence") else ""
365
+ console.print(f"[green]✓[/green] Done: [bold]{task['title']}[/bold]{next_due}")
315
366
  if _json_mode:
316
367
  _output_json(results[0] if len(task_ids) == 1 and results else results)
317
368
  if errors:
@@ -367,25 +418,60 @@ def edit(
367
418
  notes: str = typer.Option(None, "--notes", "-n", help="New notes"),
368
419
  context: int = typer.Option(None, "--context", "-c", help="Move to context ID"),
369
420
  no_context: bool = typer.Option(False, "--no-context", help="Remove context"),
421
+ due: str = typer.Option(None, "--due", "-d", help="When it's due, e.g. 'tomorrow 3pm'"),
422
+ no_due: bool = typer.Option(False, "--no-due", help="Remove due date (and repeat)"),
423
+ repeat: str = typer.Option(None, "--repeat", "-r", help="daily, weekdays, mon,thu, or 3d"),
424
+ no_repeat: bool = typer.Option(False, "--no-repeat", help="Stop repeating"),
370
425
  ):
371
- """Edit a task's title, notes, or context."""
426
+ """Edit a task's title, notes, context, due date, or repeat."""
372
427
  if no_context and context is not None:
373
428
  _validation_error("--no-context and --context cannot be used together")
429
+ if no_due and due is not None:
430
+ _validation_error("--no-due and --due cannot be used together")
431
+ if no_repeat and repeat is not None:
432
+ _validation_error("--no-repeat and --repeat cannot be used together")
374
433
 
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.")
434
+ if (title is None and notes is None and context is None and not no_context
435
+ and due is None and not no_due and repeat is None and not no_repeat):
436
+ _validation_error("No changes specified. Use --title, --notes, --context, --due, or --repeat.")
377
437
 
378
438
  if title is not None:
379
439
  title = _validate_title(title)
440
+ recurrence = _normalize_repeat(repeat) if repeat else None
380
441
 
381
442
  try:
382
443
  task = api.update_task(task_id, title=title, notes=notes,
383
- context_id=context, clear_context=no_context)
444
+ context_id=context, clear_context=no_context,
445
+ due=due, clear_due=no_due,
446
+ recurrence=recurrence, clear_recurrence=no_repeat)
447
+ except Exception as exc:
448
+ _handle_api_error(exc)
449
+ if _json_mode:
450
+ _output_json(task)
451
+ console.print(f"[green bold]Updated:[/green bold] {task['title']} [dim](#{task['id']})[/dim]{_format_due(task)}")
452
+
453
+
454
+ @app.command()
455
+ def snooze(
456
+ task_id: int = typer.Argument(..., help="ID of the task to snooze"),
457
+ duration: str = typer.Argument("10m", help="10m, 1h, 3h, 1d, or 'morning' (tomorrow morning)"),
458
+ ):
459
+ """Push a task's due time back (stops the nagging until then)."""
460
+ d = duration.strip().lower()
461
+ minutes = preset = None
462
+ if d in ("morning", "tomorrow"):
463
+ preset = "tomorrow_morning"
464
+ elif m := re.fullmatch(r"(\d+)\s*(m|min|h|hr|d)", d):
465
+ minutes = int(m.group(1)) * {"m": 1, "min": 1, "h": 60, "hr": 60, "d": 1440}[m.group(2)]
466
+ else:
467
+ _validation_error(f"Invalid duration '{duration}'. Use 10m, 1h, 3h, 1d, or morning.")
468
+ try:
469
+ task = api.snooze_task(task_id, minutes=minutes, preset=preset)
384
470
  except Exception as exc:
385
471
  _handle_api_error(exc)
386
472
  if _json_mode:
387
473
  _output_json(task)
388
- console.print(f"[green bold]Updated:[/green bold] {task['title']} [dim](#{task['id']})[/dim]")
474
+ console.print(f"[cyan]💤 Snoozed:[/cyan] [bold]{task['title']}[/bold]{_format_due(task)}")
389
475
 
390
476
 
391
477
  @app.command()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: doingapp
3
- Version: 0.6.0
3
+ Version: 0.7.1
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
@@ -73,13 +73,14 @@ doing apikey --key doing_xxxxx
73
73
  | Command | Description |
74
74
  |---------|-------------|
75
75
  | `doing` | Show active tasks (same as `doing ls`) |
76
- | `doing add <title>` | Add a task (`-n` notes, `-c` context ID) |
76
+ | `doing add <title>` | Add a task (`-n` notes, `-c` context ID, `-d "tomorrow 3pm"` due, `-r daily` repeat) |
77
77
  | `doing get <id>` | Get a single task by ID |
78
78
  | `doing done <id>` | Mark a task as done |
79
79
  | `doing pause <id>` | Pause a task |
80
80
  | `doing resume <id>` | Resume a paused task |
81
81
  | `doing reopen <id>` | Reopen a completed task |
82
- | `doing edit <id>` | Edit a task (`-t` title, `-n` notes, `-c` context, `--no-context`) |
82
+ | `doing edit <id>` | Edit a task (`-t` title, `-n` notes, `-c` context, `--no-context`, `-d` due, `--no-due`, `-r` repeat, `--no-repeat`) |
83
+ | `doing snooze <id> [10m\|1h\|3h\|1d\|morning]` | Push a due task back (default 10m) |
83
84
  | `doing rm <id>` | Delete a task |
84
85
  | `doing ls` | List active tasks (`-c` context ID) |
85
86
  | `doing log` | Show tasks done today (`-d YYYY-MM-DD`, `-c` context ID) |
@@ -93,6 +94,19 @@ doing apikey --key doing_xxxxx
93
94
  | `doing logout` | Log out |
94
95
  | `doing apikey` | Save an API key (`-k` key) |
95
96
 
97
+ ## Due dates and reminders
98
+
99
+ Give a task a due date and the Doing iOS app nags you every few minutes once it's overdue, until you mark it done or snooze it.
100
+
101
+ ```bash
102
+ doing add "Call mom" -d "tomorrow 3pm"
103
+ doing add "Water plants" -d friday -r 3d # repeats 3 days after each completion
104
+ doing add "Standup notes" -d "9am" -r weekdays
105
+ doing snooze 12 1h
106
+ ```
107
+
108
+ `-d` accepts natural language: `in 20m`, `tonight`, `fri 9am`, `oct 30`. Dates without a time use your morning time (9:00 by default). `-r` accepts `daily`, `weekdays`, days like `mon,thu`, or `Nd` (N days after done).
109
+
96
110
  ## JSON output
97
111
 
98
112
  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.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "doingapp"
7
- version = "0.6.0"
7
+ version = "0.7.1"
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