dailybot-cli 3.22.1__tar.gz → 3.23.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.
Files changed (65) hide show
  1. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/PKG-INFO +3 -1
  2. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/README.md +2 -0
  3. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/api_client.py +44 -0
  4. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/project.py +133 -2
  5. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/public_api_helpers.py +10 -0
  6. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/task.py +56 -1
  7. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/display.py +63 -0
  8. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli.egg-info/PKG-INFO +3 -1
  9. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/pyproject.toml +1 -1
  10. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/LICENSE +0 -0
  11. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/__init__.py +0 -0
  12. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/__init__.py +0 -0
  13. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/_attachments.py +0 -0
  14. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/_beta.py +0 -0
  15. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/_briefing.py +0 -0
  16. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/_destructive.py +0 -0
  17. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/_favorites.py +0 -0
  18. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/_rollups.py +0 -0
  19. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/_writes.py +0 -0
  20. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/agent.py +0 -0
  21. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/ask.py +0 -0
  22. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/auth.py +0 -0
  23. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/authoring_helpers.py +0 -0
  24. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/board.py +0 -0
  25. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/channels.py +0 -0
  26. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/chat.py +0 -0
  27. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/checkin.py +0 -0
  28. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/config.py +0 -0
  29. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/conversation.py +0 -0
  30. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/env.py +0 -0
  31. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/featured.py +0 -0
  32. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/form.py +0 -0
  33. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/goal.py +0 -0
  34. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/hook.py +0 -0
  35. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/identity.py +0 -0
  36. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/interactive.py +0 -0
  37. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/interactive_chat.py +0 -0
  38. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/kudos.py +0 -0
  39. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/label.py +0 -0
  40. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/query_options.py +0 -0
  41. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/status.py +0 -0
  42. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/tasks.py +0 -0
  43. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/team.py +0 -0
  44. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/uninstall.py +0 -0
  45. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/update.py +0 -0
  46. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/upgrade.py +0 -0
  47. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/user.py +0 -0
  48. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/user_scoped_actions.py +0 -0
  49. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/version.py +0 -0
  50. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/commands/workflow.py +0 -0
  51. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/config.py +0 -0
  52. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/install_method.py +0 -0
  53. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/ledger.py +0 -0
  54. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/main.py +0 -0
  55. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/tui/__init__.py +0 -0
  56. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/tui/app.py +0 -0
  57. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/tui/commands.py +0 -0
  58. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/tui/conversation.py +0 -0
  59. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli/tui/intents.py +0 -0
  60. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli.egg-info/SOURCES.txt +0 -0
  61. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli.egg-info/dependency_links.txt +0 -0
  62. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli.egg-info/entry_points.txt +0 -0
  63. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli.egg-info/requires.txt +0 -0
  64. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/dailybot_cli.egg-info/top_level.txt +0 -0
  65. {dailybot_cli-3.22.1 → dailybot_cli-3.23.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dailybot-cli
3
- Version: 3.22.1
3
+ Version: 3.23.1
4
4
  Summary: Dailybot CLI - The command-line bridge between humans and agents. Progress reports, observability, and workflow automation for modern teams across Slack, Google Chat, Discord, MS Teams, and more.
5
5
  Author-email: Dailybot <support@dailybot.com>
6
6
  License: MIT
@@ -991,6 +991,7 @@ humans who own them.
991
991
  | `dailybot task comment <uuid> <body>` | Comment (`-` reads the body from stdin) |
992
992
  | `dailybot task comments <uuid>` | List a task's comments |
993
993
  | `dailybot task comment-react\|comment-unreact <task> <comment> <emoji>` | React to a comment with one emoji (👍, 🚀…), or remove your reaction — **needs a person: `dailybot login` or a personal API key** |
994
+ | `dailybot task comment-reactions <task> <comment> [--emoji 👍]` | Everyone who reacted to a comment, oldest first, with the agent that reacted for them (a comment itself shows the first 10 per emoji and the true count) |
994
995
  | `dailybot task link <a> <b> --type <rel>` | Relate two tasks (`blocks`, `relates_to`, `duplicates`) |
995
996
  | `dailybot task labels <uuid> --mode add\|remove\|replace` | Change a task's labels |
996
997
  | `dailybot task participants add\|list\|remove <task>` | Who is on a task (`--role participant\|watcher`); all need a person: `dailybot login` or a personal API key |
@@ -1033,6 +1034,7 @@ humans who own them.
1033
1034
  | `dailybot project update-post <uuid> <body>` | **Post a project update** — how the team sees what was done (`--health`; sends an idempotency key) |
1034
1035
  | `dailybot project update-get\|update-edit\|update-delete <project> <update>` | Read one update (author, `via <agent>`, health, files); edit its text or health (author only); delete it (author or org admin; `--dry-run`) |
1035
1036
  | `dailybot project update-attach\|update-attachments <project> <update>` · `update-attachment get\|rename\|delete` | Files on an update (≤5 MiB). Attach and rename: author only; delete: author or an organization admin. For an inline image, attach it, then `update-edit` the body with `attachment:<uuid>` |
1037
+ | `dailybot project update-react\|update-unreact <project> <update> <emoji>` · `update-reactions <project> <update> [--emoji 👍]` | React to a project update with one emoji or remove your reaction — **needs a person: `dailybot login` or a personal API key**; list everyone who reacted |
1036
1038
  | `dailybot project milestones [<uuid>]` | List milestones |
1037
1039
  | `dailybot project milestone-complete <p> <m>` | Complete a milestone. **Its open tasks stay open** |
1038
1040
  | `dailybot project milestone-reopen <p> <m>` | Reopen a milestone |
@@ -952,6 +952,7 @@ humans who own them.
952
952
  | `dailybot task comment <uuid> <body>` | Comment (`-` reads the body from stdin) |
953
953
  | `dailybot task comments <uuid>` | List a task's comments |
954
954
  | `dailybot task comment-react\|comment-unreact <task> <comment> <emoji>` | React to a comment with one emoji (👍, 🚀…), or remove your reaction — **needs a person: `dailybot login` or a personal API key** |
955
+ | `dailybot task comment-reactions <task> <comment> [--emoji 👍]` | Everyone who reacted to a comment, oldest first, with the agent that reacted for them (a comment itself shows the first 10 per emoji and the true count) |
955
956
  | `dailybot task link <a> <b> --type <rel>` | Relate two tasks (`blocks`, `relates_to`, `duplicates`) |
956
957
  | `dailybot task labels <uuid> --mode add\|remove\|replace` | Change a task's labels |
957
958
  | `dailybot task participants add\|list\|remove <task>` | Who is on a task (`--role participant\|watcher`); all need a person: `dailybot login` or a personal API key |
@@ -994,6 +995,7 @@ humans who own them.
994
995
  | `dailybot project update-post <uuid> <body>` | **Post a project update** — how the team sees what was done (`--health`; sends an idempotency key) |
995
996
  | `dailybot project update-get\|update-edit\|update-delete <project> <update>` | Read one update (author, `via <agent>`, health, files); edit its text or health (author only); delete it (author or org admin; `--dry-run`) |
996
997
  | `dailybot project update-attach\|update-attachments <project> <update>` · `update-attachment get\|rename\|delete` | Files on an update (≤5 MiB). Attach and rename: author only; delete: author or an organization admin. For an inline image, attach it, then `update-edit` the body with `attachment:<uuid>` |
998
+ | `dailybot project update-react\|update-unreact <project> <update> <emoji>` · `update-reactions <project> <update> [--emoji 👍]` | React to a project update with one emoji or remove your reaction — **needs a person: `dailybot login` or a personal API key**; list everyone who reacted |
997
999
  | `dailybot project milestones [<uuid>]` | List milestones |
998
1000
  | `dailybot project milestone-complete <p> <m>` | Complete a milestone. **Its open tasks stay open** |
999
1001
  | `dailybot project milestone-reopen <p> <m>` | Reopen a milestone |
@@ -406,6 +406,11 @@ def _checked_emoji(value: str) -> str:
406
406
  return value
407
407
 
408
408
 
409
+ def _reaction_filter(emoji: str | None) -> dict[str, Any] | None:
410
+ """The `?emoji=` filter of a reactions list, or no filter (every emoji)."""
411
+ return {"emoji": _checked_emoji(emoji)} if emoji else None
412
+
413
+
409
414
  def _path_segment(value: Any) -> str:
410
415
  """One validated path segment: a key, a uuid or a slug — never `/`, `..`, `?`, `#`."""
411
416
  text: str = str(value)
@@ -2756,6 +2761,45 @@ class DailyBotClient:
2756
2761
  target: str = f"{base}{quote(_checked_emoji(emoji), safe='')}/"
2757
2762
  return self._tasks_write("DELETE", "", url=target)
2758
2763
 
2764
+ def list_comment_reactions(
2765
+ self, task_uuid: str, comment_uuid: str, *, emoji: str | None = None, **page: Any
2766
+ ) -> PaginatedResult:
2767
+ """GET …/comments/<c>/reactions/ — everyone who reacted, oldest first."""
2768
+ return self._tasks_list(
2769
+ f"tasks/{_path_segment(task_uuid)}/comments/{_path_segment(comment_uuid)}/reactions/",
2770
+ params=_reaction_filter(emoji),
2771
+ **page,
2772
+ )
2773
+
2774
+ def add_update_reaction(self, project_uuid: str, update_uuid: str, emoji: str) -> Any:
2775
+ """POST …/updates/<u>/reactions/ — idempotent; answers with the whole update."""
2776
+ return self._tasks_write(
2777
+ "POST",
2778
+ f"projects/{_path_segment(project_uuid)}/updates/{_path_segment(update_uuid)}"
2779
+ "/reactions/",
2780
+ json={"emoji": _checked_emoji(emoji)},
2781
+ )
2782
+
2783
+ def remove_update_reaction(self, project_uuid: str, update_uuid: str, emoji: str) -> Any:
2784
+ """DELETE …/updates/<u>/reactions/<emoji>/ — the emoji travels percent-encoded."""
2785
+ base: str = self._tasks_url(
2786
+ f"projects/{_path_segment(project_uuid)}/updates/{_path_segment(update_uuid)}"
2787
+ "/reactions/"
2788
+ )
2789
+ target: str = f"{base}{quote(_checked_emoji(emoji), safe='')}/"
2790
+ return self._tasks_write("DELETE", "", url=target)
2791
+
2792
+ def list_update_reactions(
2793
+ self, project_uuid: str, update_uuid: str, *, emoji: str | None = None, **page: Any
2794
+ ) -> PaginatedResult:
2795
+ """GET …/updates/<u>/reactions/ — everyone who reacted, oldest first."""
2796
+ return self._tasks_list(
2797
+ f"projects/{_path_segment(project_uuid)}/updates/{_path_segment(update_uuid)}"
2798
+ "/reactions/",
2799
+ params=_reaction_filter(emoji),
2800
+ **page,
2801
+ )
2802
+
2759
2803
  def get_board(self, board_uuid: str) -> dict[str, Any]:
2760
2804
  """GET /v1/tasks/boards/<uuid>/."""
2761
2805
  return self._tasks_read(f"boards/{_path_segment(board_uuid)}/")
@@ -6,7 +6,12 @@ from typing import Any
6
6
 
7
7
  import click
8
8
 
9
- from dailybot_cli.api_client import ATTACHMENT_MULTIPART_MAX_BYTES, APIError, PaginatedResult
9
+ from dailybot_cli.api_client import (
10
+ ATTACHMENT_MULTIPART_MAX_BYTES,
11
+ APIError,
12
+ PaginatedResult,
13
+ is_reaction_emoji,
14
+ )
10
15
  from dailybot_cli.commands._attachments import (
11
16
  run_attach,
12
17
  run_delete,
@@ -25,7 +30,13 @@ from dailybot_cli.commands.public_api_helpers import (
25
30
  require_auth,
26
31
  rows_of,
27
32
  )
28
- from dailybot_cli.commands.query_options import build_query_params, query_options, resolve_fetch_all
33
+ from dailybot_cli.commands.query_options import (
34
+ PAGING_ONLY_MORE_HINT,
35
+ build_query_params,
36
+ paging_options,
37
+ query_options,
38
+ resolve_fetch_all,
39
+ )
29
40
  from dailybot_cli.display import (
30
41
  console,
31
42
  print_info,
@@ -34,6 +45,7 @@ from dailybot_cli.display import (
34
45
  print_project_updates,
35
46
  print_projects_table,
36
47
  print_raw_value,
48
+ print_reaction_list,
37
49
  print_success,
38
50
  print_tasks_detail_panel,
39
51
  print_tasks_rows,
@@ -1331,6 +1343,125 @@ def project_update_edit(
1331
1343
  report_write(data, "Project update edited")
1332
1344
 
1333
1345
 
1346
+ _NOT_ONE_EMOJI: str = "A reaction must be one emoji, e.g. 👍 (no text or :shortcodes:)."
1347
+
1348
+
1349
+ @project.command("update-react")
1350
+ @click.argument("project_uuid", metavar="PROJECT")
1351
+ @click.argument("update_uuid", metavar="UPDATE")
1352
+ @click.argument("emoji")
1353
+ @click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
1354
+ def project_update_react(project_uuid: str, update_uuid: str, emoji: str, json_mode: bool) -> None:
1355
+ """React to a project update with one emoji. Needs a person: `dailybot login` or a personal API key.
1356
+
1357
+ \b
1358
+ Emoji only (no text or :shortcodes:). Reacting twice with the same emoji is
1359
+ safe: nothing changes. One person may hold only a limited number of different
1360
+ emojis on one project update; if the server refuses with `reaction_limit_reached`,
1361
+ remove one of yours first (`dailybot project update-unreact`).
1362
+
1363
+ \b
1364
+ Examples:
1365
+ dailybot project update-react <project-uuid> <update-uuid> 👍
1366
+ dailybot project update-react <project-uuid> <update-uuid> 🚀 --json
1367
+ """
1368
+ if not is_reaction_emoji(emoji):
1369
+ raise click.UsageError(_NOT_ONE_EMOJI)
1370
+ client = require_auth()
1371
+ try:
1372
+ with console.status("Reacting..."):
1373
+ data: Any = client.add_update_reaction(project_uuid, update_uuid, emoji)
1374
+ except APIError as exc:
1375
+ exit_for_tasks_error(exc, json_mode)
1376
+ if json_mode:
1377
+ emit_json(data)
1378
+ return
1379
+ print_success(f"Reacted {emoji}.")
1380
+
1381
+
1382
+ @project.command("update-unreact")
1383
+ @click.argument("project_uuid", metavar="PROJECT")
1384
+ @click.argument("update_uuid", metavar="UPDATE")
1385
+ @click.argument("emoji")
1386
+ @click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
1387
+ def project_update_unreact(
1388
+ project_uuid: str, update_uuid: str, emoji: str, json_mode: bool
1389
+ ) -> None:
1390
+ """Remove your emoji reaction from a project update. Needs a person: `dailybot login` or a personal API key.
1391
+
1392
+ \b
1393
+ Safe to repeat: removing a reaction you did not leave changes nothing.
1394
+
1395
+ \b
1396
+ Examples:
1397
+ dailybot project update-unreact <project-uuid> <update-uuid> 👍
1398
+ """
1399
+ if not is_reaction_emoji(emoji):
1400
+ raise click.UsageError(_NOT_ONE_EMOJI)
1401
+ client = require_auth()
1402
+ try:
1403
+ with console.status("Removing the reaction..."):
1404
+ client.remove_update_reaction(project_uuid, update_uuid, emoji)
1405
+ except APIError as exc:
1406
+ exit_for_tasks_error(exc, json_mode)
1407
+ if json_mode:
1408
+ emit_json({"removed": True, "project": project_uuid, "update": update_uuid, "emoji": emoji})
1409
+ return
1410
+ print_success(f"Removed {emoji}.")
1411
+
1412
+
1413
+ @project.command("update-reactions")
1414
+ @click.argument("project_uuid", metavar="PROJECT")
1415
+ @click.argument("update_uuid", metavar="UPDATE")
1416
+ @click.option("--emoji", default=None, help="Only this emoji (all emojis when omitted).")
1417
+ @click.option("--all", "-a", "fetch_all", is_flag=True, help="Fetch every page.")
1418
+ @paging_options
1419
+ @click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
1420
+ def project_update_reactions(
1421
+ project_uuid: str, update_uuid: str, emoji: str | None, json_mode: bool, **flags: Any
1422
+ ) -> None:
1423
+ """Everyone who reacted to a project update, oldest first, with the agent that reacted for them.
1424
+
1425
+ \b
1426
+ An update itself carries at most the first few reactors per emoji; this lists
1427
+ them all.
1428
+
1429
+ \b
1430
+ Examples:
1431
+ dailybot project update-reactions <project-uuid> <update-uuid>
1432
+ dailybot project update-reactions <project-uuid> <update-uuid> --emoji 👍 --json
1433
+ """
1434
+ if emoji is not None and not is_reaction_emoji(emoji):
1435
+ raise click.UsageError("--emoji must be one emoji, e.g. 👍 (no text or :shortcodes:).")
1436
+ client = require_auth()
1437
+ try:
1438
+ spec = build_query_params(**flags)
1439
+ with console.status("Reading reactions..."):
1440
+ result: PaginatedResult = client.list_update_reactions(
1441
+ project_uuid,
1442
+ update_uuid,
1443
+ emoji=emoji,
1444
+ page=spec.page,
1445
+ page_size=spec.page_size,
1446
+ fetch_all=spec.fetch_all,
1447
+ limit=spec.limit,
1448
+ )
1449
+ except ValueError as exc:
1450
+ raise click.BadParameter(str(exc)) from exc
1451
+ except APIError as exc:
1452
+ exit_for_tasks_error(exc, json_mode)
1453
+ if json_mode:
1454
+ emit_json(_envelope(result))
1455
+ return
1456
+ print_reaction_list(result.results)
1457
+ print_pagination_footer(
1458
+ len(result.results),
1459
+ result.count,
1460
+ has_more=bool(result.next),
1461
+ more_hint=PAGING_ONLY_MORE_HINT,
1462
+ )
1463
+
1464
+
1334
1465
  @project.command("update-delete")
1335
1466
  @click.argument("project_uuid", metavar="PROJECT")
1336
1467
  @click.argument("update_uuid", metavar="UPDATE")
@@ -60,6 +60,12 @@ ERROR_CODE_MESSAGES: dict[str, str] = {
60
60
  "reaction_invalid_emoji": (
61
61
  "A reaction must be one emoji (for example 👍 or 🚀), not text or a :shortcode:."
62
62
  ),
63
+ "reaction_limit_reached": (
64
+ "You already hold the most different emojis allowed on this comment or update. "
65
+ "Remove one of yours (`dailybot task comment-unreact` or "
66
+ "`dailybot project update-unreact`) before adding another; "
67
+ "re-adding an emoji you already hold changes nothing."
68
+ ),
63
69
  "label_in_use": (
64
70
  "This label is still in use, so it cannot be deleted. Archive it instead "
65
71
  "(`dailybot label archive <label>` for organization labels, or "
@@ -541,6 +547,10 @@ def _augment_code_message(base: str, code: str, extra: dict[str, Any]) -> str:
541
547
  if current:
542
548
  detail_bits += f" Your role: {current}."
543
549
  return f"{base}{detail_bits}"
550
+ elif code == "reaction_limit_reached":
551
+ limit: Any = extra.get("limit")
552
+ if isinstance(limit, int):
553
+ return f"{base} The limit is {limit} different emojis per person."
544
554
  return base
545
555
 
546
556
 
@@ -67,6 +67,7 @@ from dailybot_cli.display import (
67
67
  print_deprecation,
68
68
  print_error,
69
69
  print_pagination_footer,
70
+ print_reaction_list,
70
71
  print_success,
71
72
  print_task_briefing,
72
73
  print_task_comments,
@@ -1217,7 +1218,9 @@ def task_comment_react(task_uuid: str, comment_uuid: str, emoji: str, json_mode:
1217
1218
 
1218
1219
  \b
1219
1220
  Emoji only (no text or :shortcodes:). Reacting twice with the same emoji is
1220
- safe: nothing changes.
1221
+ safe: nothing changes. One person may hold only a limited number of different
1222
+ emojis on one comment; if the server refuses with `reaction_limit_reached`,
1223
+ remove one of yours first (`dailybot task comment-unreact`).
1221
1224
 
1222
1225
  \b
1223
1226
  Examples:
@@ -1267,6 +1270,58 @@ def task_comment_unreact(task_uuid: str, comment_uuid: str, emoji: str, json_mod
1267
1270
  print_success(f"Removed {emoji}.")
1268
1271
 
1269
1272
 
1273
+ @task.command("comment-reactions")
1274
+ @click.argument("task_uuid", metavar="TASK")
1275
+ @click.argument("comment_uuid", metavar="COMMENT")
1276
+ @click.option("--emoji", default=None, help="Only this emoji (all emojis when omitted).")
1277
+ @click.option("--all", "-a", "fetch_all", is_flag=True, help="Fetch every page.")
1278
+ @paging_options
1279
+ @click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
1280
+ def task_comment_reactions(
1281
+ task_uuid: str, comment_uuid: str, emoji: str | None, json_mode: bool, **flags: Any
1282
+ ) -> None:
1283
+ """Everyone who reacted to a comment, oldest first, with the agent that reacted for them.
1284
+
1285
+ \b
1286
+ A comment itself carries at most the first few reactors per emoji; this lists
1287
+ them all.
1288
+
1289
+ \b
1290
+ Examples:
1291
+ dailybot task comment-reactions ENG-142 <comment-uuid>
1292
+ dailybot task comment-reactions ENG-142 <comment-uuid> --emoji 👍 --json
1293
+ """
1294
+ if emoji is not None and not is_reaction_emoji(emoji):
1295
+ raise click.UsageError("--emoji must be one emoji, e.g. 👍 (no text or :shortcodes:).")
1296
+ client = require_auth()
1297
+ try:
1298
+ spec = build_query_params(**flags)
1299
+ with console.status("Reading reactions..."):
1300
+ result: PaginatedResult = client.list_comment_reactions(
1301
+ task_uuid,
1302
+ comment_uuid,
1303
+ emoji=emoji,
1304
+ page=spec.page,
1305
+ page_size=spec.page_size,
1306
+ fetch_all=spec.fetch_all,
1307
+ limit=spec.limit,
1308
+ )
1309
+ except ValueError as exc:
1310
+ raise click.BadParameter(str(exc)) from exc
1311
+ except APIError as exc:
1312
+ exit_for_tasks_error(exc, json_mode)
1313
+ if json_mode:
1314
+ emit_json(_envelope(result))
1315
+ return
1316
+ print_reaction_list(result.results)
1317
+ print_pagination_footer(
1318
+ len(result.results),
1319
+ result.count,
1320
+ has_more=bool(result.next),
1321
+ more_hint=PAGING_ONLY_MORE_HINT,
1322
+ )
1323
+
1324
+
1270
1325
  @task.command("comment-delete")
1271
1326
  @click.argument("task_uuid", metavar="TASK")
1272
1327
  @click.argument("comment_uuid", metavar="COMMENT")
@@ -1623,6 +1623,63 @@ def _person_name(person: dict[str, Any]) -> Any:
1623
1623
  return person.get("full_name") or person.get("name")
1624
1624
 
1625
1625
 
1626
+ def _reactor(user: Any, agent: Any = None) -> str:
1627
+ """One person who reacted, with the agent that reacted for them when stamped."""
1628
+ name: Any = _person_name(user) if isinstance(user, dict) else user
1629
+ who: str = present_untrusted(name, limit=24)
1630
+ stamp: Any = (
1631
+ agent
1632
+ if agent is not None
1633
+ else (user.get("executed_by_agent") if isinstance(user, dict) else None)
1634
+ )
1635
+ if isinstance(stamp, dict) and stamp.get("name"):
1636
+ who += f" [dim]via {present_untrusted(stamp.get('name'), limit=24)}[/dim]"
1637
+ return who
1638
+
1639
+
1640
+ def format_reactions(reactions: Any) -> str:
1641
+ """One line for a comment's or update's reactions: emoji, true count, who reacted.
1642
+
1643
+ `users` holds at most the first few reactors; `count` is always the true total,
1644
+ so the list was cut exactly when `count > len(users)`. Names are user-authored
1645
+ and quoted as data. An older server sends no `users` at all: the count alone is shown.
1646
+ """
1647
+ if not isinstance(reactions, list):
1648
+ return ""
1649
+ parts: list[str] = []
1650
+ for entry in reactions:
1651
+ if not isinstance(entry, dict) or not entry.get("emoji"):
1652
+ continue
1653
+ count: Any = entry.get("count")
1654
+ users: Any = entry.get("users")
1655
+ part: str = (
1656
+ f"{safe_text(entry.get('emoji'))} {safe_text(count if count is not None else '')}"
1657
+ )
1658
+ if isinstance(users, list):
1659
+ names: list[str] = [_reactor(u) for u in users]
1660
+ if isinstance(count, int) and count > len(users):
1661
+ names.append(f"+{count - len(users)} more")
1662
+ if names:
1663
+ part += f" ({', '.join(names)})"
1664
+ if entry.get("reacted"):
1665
+ part += " [dim]you reacted[/dim]"
1666
+ parts.append(part.strip())
1667
+ return " · ".join(parts)
1668
+
1669
+
1670
+ def print_reaction_list(entries: list[dict[str, Any]]) -> None:
1671
+ """Render a full reactor list: emoji, the person (and agent), and when."""
1672
+ if not entries:
1673
+ print_info("No reactions.")
1674
+ return
1675
+ for entry in entries:
1676
+ who: str = _reactor(entry.get("user") or {}, entry.get("executed_by_agent"))
1677
+ console.print(
1678
+ f"{safe_text(entry.get('emoji') or '')} {who} "
1679
+ f"[dim]{safe_text(entry.get('created_at') or '')}[/dim]"
1680
+ )
1681
+
1682
+
1626
1683
  def _state_name(task: dict[str, Any]) -> str:
1627
1684
  state: Any = task.get("state")
1628
1685
  if isinstance(state, dict):
@@ -2106,6 +2163,9 @@ def print_project_updates(updates: list[dict[str, Any]]) -> None:
2106
2163
  meta.append(f"{count} file(s)")
2107
2164
  console.print(f"[dim]{' · '.join(m for m in meta if m)}[/dim] {who}")
2108
2165
  console.print(f" {present_untrusted(update.get('body'), limit=400)}")
2166
+ reacted: str = format_reactions(update.get("reactions"))
2167
+ if reacted:
2168
+ console.print(f" {reacted}")
2109
2169
  if update.get("uuid"):
2110
2170
  console.print(f" [dim]{safe_text(update.get('uuid'))}[/dim]")
2111
2171
 
@@ -2160,3 +2220,6 @@ def print_task_comments(comments: list[dict[str, Any]]) -> None:
2160
2220
  # A reply names its thread's root in `parent_comment`; `_threaded` put it there.
2161
2221
  thread: str = " ↳ " if comment.get("parent_comment") else ""
2162
2222
  console.print(f"{thread}{attribution}: {present_untrusted(comment.get('body'), limit=400)}")
2223
+ reacted: str = format_reactions(comment.get("reactions"))
2224
+ if reacted:
2225
+ console.print(f"{' ' if thread else ' '}{reacted}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dailybot-cli
3
- Version: 3.22.1
3
+ Version: 3.23.1
4
4
  Summary: Dailybot CLI - The command-line bridge between humans and agents. Progress reports, observability, and workflow automation for modern teams across Slack, Google Chat, Discord, MS Teams, and more.
5
5
  Author-email: Dailybot <support@dailybot.com>
6
6
  License: MIT
@@ -991,6 +991,7 @@ humans who own them.
991
991
  | `dailybot task comment <uuid> <body>` | Comment (`-` reads the body from stdin) |
992
992
  | `dailybot task comments <uuid>` | List a task's comments |
993
993
  | `dailybot task comment-react\|comment-unreact <task> <comment> <emoji>` | React to a comment with one emoji (👍, 🚀…), or remove your reaction — **needs a person: `dailybot login` or a personal API key** |
994
+ | `dailybot task comment-reactions <task> <comment> [--emoji 👍]` | Everyone who reacted to a comment, oldest first, with the agent that reacted for them (a comment itself shows the first 10 per emoji and the true count) |
994
995
  | `dailybot task link <a> <b> --type <rel>` | Relate two tasks (`blocks`, `relates_to`, `duplicates`) |
995
996
  | `dailybot task labels <uuid> --mode add\|remove\|replace` | Change a task's labels |
996
997
  | `dailybot task participants add\|list\|remove <task>` | Who is on a task (`--role participant\|watcher`); all need a person: `dailybot login` or a personal API key |
@@ -1033,6 +1034,7 @@ humans who own them.
1033
1034
  | `dailybot project update-post <uuid> <body>` | **Post a project update** — how the team sees what was done (`--health`; sends an idempotency key) |
1034
1035
  | `dailybot project update-get\|update-edit\|update-delete <project> <update>` | Read one update (author, `via <agent>`, health, files); edit its text or health (author only); delete it (author or org admin; `--dry-run`) |
1035
1036
  | `dailybot project update-attach\|update-attachments <project> <update>` · `update-attachment get\|rename\|delete` | Files on an update (≤5 MiB). Attach and rename: author only; delete: author or an organization admin. For an inline image, attach it, then `update-edit` the body with `attachment:<uuid>` |
1037
+ | `dailybot project update-react\|update-unreact <project> <update> <emoji>` · `update-reactions <project> <update> [--emoji 👍]` | React to a project update with one emoji or remove your reaction — **needs a person: `dailybot login` or a personal API key**; list everyone who reacted |
1036
1038
  | `dailybot project milestones [<uuid>]` | List milestones |
1037
1039
  | `dailybot project milestone-complete <p> <m>` | Complete a milestone. **Its open tasks stay open** |
1038
1040
  | `dailybot project milestone-reopen <p> <m>` | Reopen a milestone |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dailybot-cli"
7
- version = "3.22.1"
7
+ version = "3.23.1"
8
8
  description = "Dailybot CLI - The command-line bridge between humans and agents. Progress reports, observability, and workflow automation for modern teams across Slack, Google Chat, Discord, MS Teams, and more."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
File without changes
File without changes