doingapp 0.4.0__tar.gz → 0.5.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.4.0
3
+ Version: 0.5.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
@@ -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
 
@@ -42,6 +42,13 @@ def login(email: str, password: str) -> dict:
42
42
  return r.json()
43
43
 
44
44
 
45
+ def get_me() -> dict:
46
+ with _client() as c:
47
+ r = c.get("/auth/me")
48
+ r.raise_for_status()
49
+ return r.json()
50
+
51
+
45
52
  def create_task(title: str, notes: str | None = None, context_id: int | None = None) -> dict:
46
53
  payload: dict = {"title": title}
47
54
  if notes is not None:
@@ -54,6 +61,13 @@ def create_task(title: str, notes: str | None = None, context_id: int | None = N
54
61
  return r.json()
55
62
 
56
63
 
64
+ def get_task(task_id: int) -> dict:
65
+ with _client() as c:
66
+ r = c.get(f"/tasks/{task_id}")
67
+ r.raise_for_status()
68
+ return r.json()
69
+
70
+
57
71
  def list_tasks(status: str | None = None, date: str | None = None,
58
72
  context_id: int | None = None) -> list[dict]:
59
73
  params: dict = {"tz": _local_tz()}
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import sys
2
3
 
3
4
  import httpx
4
5
  import typer
@@ -19,15 +20,41 @@ console = Console()
19
20
  # Commands that don't require authentication
20
21
  AUTH_COMMANDS = {"login", "register", "logout", "apikey"}
21
22
 
23
+ # Global JSON mode flag, set by --json on any command
24
+ _json_mode = False
25
+
26
+
27
+ def _output_json(data) -> None:
28
+ """Write raw JSON to stdout (no Rich formatting) and exit."""
29
+ sys.stdout.write(json.dumps(data) + "\n")
30
+ raise typer.Exit()
31
+
22
32
 
23
33
  def _require_auth():
24
34
  """Exit with a message if the user is not logged in."""
25
35
  if load_token() is None:
36
+ if _json_mode:
37
+ _output_json({"error": True, "detail": "Not logged in"})
26
38
  console.print("[bold red]Not logged in.[/bold red] Run: [bold]doing login[/bold]")
27
39
  raise typer.Exit(code=1)
28
40
 
29
41
 
30
42
  def _handle_api_error(exc: Exception):
43
+ if _json_mode:
44
+ error: dict = {"error": True}
45
+ if isinstance(exc, httpx.HTTPStatusError):
46
+ error["status"] = exc.response.status_code
47
+ try:
48
+ error["detail"] = exc.response.json().get("detail", exc.response.text)
49
+ except Exception:
50
+ error["detail"] = exc.response.text
51
+ elif isinstance(exc, (httpx.ConnectError, httpx.TimeoutException)):
52
+ error["detail"] = f"Could not connect to {api.BASE_URL}"
53
+ else:
54
+ error["detail"] = str(exc)
55
+ sys.stdout.write(json.dumps(error) + "\n")
56
+ raise typer.Exit(code=1)
57
+
31
58
  if isinstance(exc, httpx.HTTPStatusError):
32
59
  status = exc.response.status_code
33
60
  if status == 401:
@@ -48,6 +75,30 @@ def _handle_api_error(exc: Exception):
48
75
  raise typer.Exit(code=1)
49
76
 
50
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
+
51
102
  def _print_notes(task: dict) -> None:
52
103
  if task.get("notes"):
53
104
  console.print(f" [dim]{task['notes']}[/dim]")
@@ -72,11 +123,16 @@ def _print_done_task(task: dict) -> None:
72
123
 
73
124
 
74
125
  @app.callback()
75
- def main(ctx: typer.Context):
126
+ def main(
127
+ ctx: typer.Context,
128
+ json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON (for scripting and AI agents)"),
129
+ ):
76
130
  """Doing - track what you're working on."""
131
+ global _json_mode
132
+ _json_mode = json_output
77
133
  if ctx.invoked_subcommand is None:
78
134
  _require_auth()
79
- ls(context=None, json_output=False)
135
+ ls()
80
136
  elif ctx.invoked_subcommand not in AUTH_COMMANDS:
81
137
  _require_auth()
82
138
 
@@ -91,6 +147,8 @@ def login():
91
147
  except Exception as exc:
92
148
  _handle_api_error(exc)
93
149
  save_token(data["access_token"])
150
+ if _json_mode:
151
+ _output_json({"ok": True, "email": email})
94
152
  console.print(f"[bold green]Logged in[/bold green] as [bold]{email}[/bold]")
95
153
 
96
154
 
@@ -101,6 +159,8 @@ def register():
101
159
  password = typer.prompt("Password", hide_input=True)
102
160
  password_confirm = typer.prompt("Confirm password", hide_input=True)
103
161
  if password != password_confirm:
162
+ if _json_mode:
163
+ _output_json({"error": True, "detail": "Passwords do not match"})
104
164
  console.print("[bold red]Passwords do not match.[/bold red]")
105
165
  raise typer.Exit(code=1)
106
166
  try:
@@ -108,6 +168,8 @@ def register():
108
168
  except Exception as exc:
109
169
  _handle_api_error(exc)
110
170
  save_token(data["access_token"])
171
+ if _json_mode:
172
+ _output_json({"ok": True, "email": email})
111
173
  console.print(f"[bold green]Account created![/bold green] Logged in as [bold]{email}[/bold]")
112
174
 
113
175
 
@@ -115,6 +177,8 @@ def register():
115
177
  def logout():
116
178
  """Log out and clear stored credentials."""
117
179
  clear_token()
180
+ if _json_mode:
181
+ _output_json({"ok": True})
118
182
  console.print("[dim]Logged out.[/dim]")
119
183
 
120
184
 
@@ -126,12 +190,30 @@ def apikey(
126
190
  if key is None:
127
191
  key = typer.prompt("API key", hide_input=True)
128
192
  if not key.startswith("doing_"):
193
+ if _json_mode:
194
+ _output_json({"error": True, "detail": "Invalid API key. Keys start with doing_"})
129
195
  console.print("[bold red]Invalid API key.[/bold red] Keys start with [bold]doing_[/bold]")
130
196
  raise typer.Exit(code=1)
131
197
  save_token(key)
198
+ if _json_mode:
199
+ _output_json({"ok": True})
132
200
  console.print("[bold green]API key saved.[/bold green] You're all set.")
133
201
 
134
202
 
203
+ @app.command()
204
+ def me():
205
+ """Show current user info (useful for verifying auth)."""
206
+ try:
207
+ user = api.get_me()
208
+ except Exception as exc:
209
+ _handle_api_error(exc)
210
+ if _json_mode:
211
+ _output_json(user)
212
+ console.print(f" [bold]{user['email']}[/bold] [dim](#{user['id']})[/dim]")
213
+ if user.get("is_admin"):
214
+ console.print(" [yellow]Admin[/yellow]")
215
+
216
+
135
217
  @app.command()
136
218
  def add(
137
219
  title: str = typer.Argument(..., help="What are you working on?"),
@@ -139,26 +221,47 @@ def add(
139
221
  context: int = typer.Option(None, "--context", "-c", help="Context ID"),
140
222
  ):
141
223
  """Add something to your plate."""
224
+ title = _validate_title(title)
142
225
  try:
143
226
  task = api.create_task(title, notes, context)
144
227
  except Exception as exc:
145
228
  _handle_api_error(exc)
229
+ if _json_mode:
230
+ _output_json(task)
146
231
  console.print(f"[green bold]●[/green bold] Added: [bold]{task['title']}[/bold] [dim](#{task['id']})[/dim]")
147
232
 
148
233
 
234
+ @app.command()
235
+ def get(
236
+ task_id: int = typer.Argument(..., help="ID of the task to fetch"),
237
+ ):
238
+ """Get a single task by ID."""
239
+ try:
240
+ task = api.get_task(task_id)
241
+ except Exception as exc:
242
+ _handle_api_error(exc)
243
+ if _json_mode:
244
+ _output_json(task)
245
+ status_icon = {"active": "[green bold]●[/green bold]", "paused": "[yellow]⏸[/yellow]", "done": "[green]✓[/green]"}
246
+ icon = status_icon.get(task["status"], "?")
247
+ ctx = f" [dim][{task['context_name']}][/dim]" if task.get("context_name") else ""
248
+ console.print(f" {icon} [dim]#{task['id']}[/dim] {task['title']}{ctx}")
249
+ if task.get("notes"):
250
+ console.print(f" [dim]{task['notes']}[/dim]")
251
+ console.print(f" [dim]Status: {task['status']} Created: {task['created_at']}[/dim]")
252
+
253
+
149
254
  @app.command()
150
255
  def ls(
151
256
  context: int = typer.Option(None, "--context", "-c", help="Filter by context ID"),
152
- json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
153
257
  ):
154
258
  """Show what's on your plate right now."""
155
259
  try:
156
260
  tasks = api.list_tasks(status="active", context_id=context)
157
261
  except Exception as exc:
158
262
  _handle_api_error(exc)
159
- if json_output:
160
- console.print_json(json.dumps(tasks))
161
- raise typer.Exit()
263
+ if _json_mode:
264
+ _output_json(tasks)
162
265
  if not tasks:
163
266
  console.print("Nothing on your plate. Add something: [bold]doing add 'task name'[/bold]")
164
267
  raise typer.Exit()
@@ -170,7 +273,6 @@ def ls(
170
273
  def log(
171
274
  date: str = typer.Option(None, "--date", "-d", help="Date (YYYY-MM-DD), defaults to today"),
172
275
  context: int = typer.Option(None, "--context", "-c", help="Filter by context ID"),
173
- json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
174
276
  ):
175
277
  """List tasks marked done today (or on a given date)."""
176
278
  from datetime import date as date_cls
@@ -179,9 +281,8 @@ def log(
179
281
  tasks = api.list_tasks(status="done", date=query_date, context_id=context)
180
282
  except Exception as exc:
181
283
  _handle_api_error(exc)
182
- if json_output:
183
- console.print_json(json.dumps(tasks))
184
- raise typer.Exit()
284
+ if _json_mode:
285
+ _output_json(tasks)
185
286
  if not tasks:
186
287
  label = query_date if date else "today"
187
288
  console.print(f"Nothing marked done {label}.")
@@ -199,6 +300,8 @@ def done(
199
300
  task = api.mark_done(task_id)
200
301
  except Exception as exc:
201
302
  _handle_api_error(exc)
303
+ if _json_mode:
304
+ _output_json(task)
202
305
  console.print(f"[green]✓[/green] Done: [bold]{task['title']}[/bold]")
203
306
 
204
307
 
@@ -211,6 +314,8 @@ def reopen(
211
314
  task = api.reopen_task(task_id)
212
315
  except Exception as exc:
213
316
  _handle_api_error(exc)
317
+ if _json_mode:
318
+ _output_json(task)
214
319
  console.print(f"[green bold]●[/green bold] Reopened: [bold]{task['title']}[/bold]")
215
320
 
216
321
 
@@ -223,6 +328,8 @@ def pause(
223
328
  task = api.pause_task(task_id)
224
329
  except Exception as exc:
225
330
  _handle_api_error(exc)
331
+ if _json_mode:
332
+ _output_json(task)
226
333
  console.print(f"[yellow]⏸[/yellow] Paused: [bold]{task['title']}[/bold]")
227
334
 
228
335
 
@@ -235,6 +342,8 @@ def resume(
235
342
  task = api.resume_task(task_id)
236
343
  except Exception as exc:
237
344
  _handle_api_error(exc)
345
+ if _json_mode:
346
+ _output_json(task)
238
347
  console.print(f"[green bold]●[/green bold] Resumed: [bold]{task['title']}[/bold]")
239
348
 
240
349
 
@@ -247,18 +356,29 @@ def edit(
247
356
  no_context: bool = typer.Option(False, "--no-context", help="Remove context"),
248
357
  ):
249
358
  """Edit a task's title, notes, or context."""
359
+ if no_context and context is not None:
360
+ _validation_error("--no-context and --context cannot be used together")
361
+
362
+ if title is None and notes is None and context is None and not no_context:
363
+ _validation_error("No changes specified. Use --title, --notes, --context, or --no-context.")
364
+
365
+ # BUG-8: Validate title if provided
366
+ if title is not None:
367
+ title = _validate_title(title)
368
+
250
369
  try:
251
370
  task = api.update_task(task_id, title=title, notes=notes,
252
371
  context_id=context, clear_context=no_context)
253
372
  except Exception as exc:
254
373
  _handle_api_error(exc)
374
+ if _json_mode:
375
+ _output_json(task)
255
376
  console.print(f"[green bold]Updated:[/green bold] {task['title']} [dim](#{task['id']})[/dim]")
256
377
 
257
378
 
258
379
  @app.command()
259
380
  def today(
260
381
  context: int = typer.Option(None, "--context", "-c", help="Filter by context ID"),
261
- json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
262
382
  ):
263
383
  """Show everything from today, grouped by status."""
264
384
  try:
@@ -266,9 +386,8 @@ def today(
266
386
  except Exception as exc:
267
387
  _handle_api_error(exc)
268
388
 
269
- if json_output:
270
- console.print_json(json.dumps(data))
271
- raise typer.Exit()
389
+ if _json_mode:
390
+ _output_json(data)
272
391
 
273
392
  active = data.get("active", [])
274
393
  paused = data.get("paused", [])
@@ -305,6 +424,8 @@ def rm(
305
424
  api.delete_task(task_id)
306
425
  except Exception as exc:
307
426
  _handle_api_error(exc)
427
+ if _json_mode:
428
+ _output_json({"deleted": True, "id": task_id})
308
429
  console.print(f"[dim]Deleted task #{task_id}.[/dim]")
309
430
 
310
431
 
@@ -317,16 +438,25 @@ def clear():
317
438
  _handle_api_error(exc)
318
439
 
319
440
  if not tasks:
441
+ if _json_mode:
442
+ _output_json([])
320
443
  console.print("Nothing to clear.")
321
444
  raise typer.Exit()
322
445
 
446
+ results = []
323
447
  for t in tasks:
324
448
  try:
325
- api.mark_done(t["id"])
449
+ result = api.mark_done(t["id"])
450
+ results.append(result)
326
451
  except Exception:
327
- console.print(f"[red]Failed to complete task #{t['id']}[/red]")
452
+ if not _json_mode:
453
+ console.print(f"[red]Failed to complete task #{t['id']}[/red]")
328
454
  continue
329
- console.print(f"[green]✓[/green] Done: [bold]{t['title']}[/bold]")
455
+ if not _json_mode:
456
+ console.print(f"[green]✓[/green] Done: [bold]{t['title']}[/bold]")
457
+
458
+ if _json_mode:
459
+ _output_json(results)
330
460
 
331
461
  console.print(f"\n[dim]Cleared {len(tasks)} task(s).[/dim]")
332
462
 
@@ -338,6 +468,8 @@ def contexts():
338
468
  ctx_list = api.list_contexts()
339
469
  except Exception as exc:
340
470
  _handle_api_error(exc)
471
+ if _json_mode:
472
+ _output_json(ctx_list)
341
473
  if not ctx_list:
342
474
  console.print("No contexts. Create one: [bold]doing context add 'name'[/bold]")
343
475
  raise typer.Exit()
@@ -350,11 +482,13 @@ def context_add(
350
482
  name: str = typer.Argument(..., help="Context name"),
351
483
  ):
352
484
  """Create a new context."""
353
- _require_auth()
485
+ name = _validate_name(name)
354
486
  try:
355
487
  ctx = api.create_context(name)
356
488
  except Exception as exc:
357
489
  _handle_api_error(exc)
490
+ if _json_mode:
491
+ _output_json(ctx)
358
492
  console.print(f"[green bold]Created context:[/green bold] {ctx['name']} [dim](#{ctx['id']})[/dim]")
359
493
 
360
494
 
@@ -363,11 +497,12 @@ def context_rm(
363
497
  context_id: int = typer.Argument(..., help="Context ID to delete"),
364
498
  ):
365
499
  """Delete a context (tasks keep their data, just lose the context)."""
366
- _require_auth()
367
500
  try:
368
501
  api.delete_context(context_id)
369
502
  except Exception as exc:
370
503
  _handle_api_error(exc)
504
+ if _json_mode:
505
+ _output_json({"deleted": True, "id": context_id})
371
506
  console.print(f"[dim]Deleted context #{context_id}.[/dim]")
372
507
 
373
508
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: doingapp
3
- Version: 0.4.0
3
+ Version: 0.5.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
@@ -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.4.0"
7
+ version = "0.5.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