dailybot-cli 3.26.1__tar.gz → 3.27.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.
Files changed (75) hide show
  1. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/PKG-INFO +1 -1
  2. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/api_client.py +178 -0
  3. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/board.py +203 -0
  4. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/plan.py +6 -3
  5. dailybot_cli-3.27.0/dailybot_cli/commands/plan_label.py +234 -0
  6. dailybot_cli-3.27.0/dailybot_cli/commands/plan_views.py +173 -0
  7. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/project.py +158 -0
  8. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/tasks.py +139 -0
  9. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli.egg-info/PKG-INFO +1 -1
  10. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli.egg-info/SOURCES.txt +2 -0
  11. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/pyproject.toml +1 -1
  12. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/LICENSE +0 -0
  13. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/README.md +0 -0
  14. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/__init__.py +0 -0
  15. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/__init__.py +0 -0
  16. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/_attachments.py +0 -0
  17. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/_beta.py +0 -0
  18. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/_briefing.py +0 -0
  19. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/_channels.py +0 -0
  20. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/_destructive.py +0 -0
  21. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/_favorites.py +0 -0
  22. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/_outbound.py +0 -0
  23. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/_paging.py +0 -0
  24. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/_refs.py +0 -0
  25. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/_rollups.py +0 -0
  26. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/_schedule.py +0 -0
  27. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/_sorting.py +0 -0
  28. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/_writes.py +0 -0
  29. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/agent.py +0 -0
  30. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/ask.py +0 -0
  31. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/auth.py +0 -0
  32. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/authoring_helpers.py +0 -0
  33. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/channels.py +0 -0
  34. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/chat.py +0 -0
  35. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/checkin.py +0 -0
  36. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/config.py +0 -0
  37. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/conversation.py +0 -0
  38. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/env.py +0 -0
  39. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/featured.py +0 -0
  40. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/form.py +0 -0
  41. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/goal.py +0 -0
  42. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/hook.py +0 -0
  43. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/identity.py +0 -0
  44. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/interactive.py +0 -0
  45. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/interactive_chat.py +0 -0
  46. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/kudos.py +0 -0
  47. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/label.py +0 -0
  48. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/public_api_helpers.py +0 -0
  49. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/query_options.py +0 -0
  50. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/status.py +0 -0
  51. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/task.py +0 -0
  52. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/tasks_settings.py +0 -0
  53. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/team.py +0 -0
  54. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/uninstall.py +0 -0
  55. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/update.py +0 -0
  56. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/upgrade.py +0 -0
  57. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/user.py +0 -0
  58. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/user_scoped_actions.py +0 -0
  59. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/version.py +0 -0
  60. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/commands/workflow.py +0 -0
  61. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/config.py +0 -0
  62. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/display.py +0 -0
  63. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/install_method.py +0 -0
  64. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/ledger.py +0 -0
  65. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/main.py +0 -0
  66. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/tui/__init__.py +0 -0
  67. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/tui/app.py +0 -0
  68. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/tui/commands.py +0 -0
  69. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/tui/conversation.py +0 -0
  70. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli/tui/intents.py +0 -0
  71. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli.egg-info/dependency_links.txt +0 -0
  72. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli.egg-info/entry_points.txt +0 -0
  73. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli.egg-info/requires.txt +0 -0
  74. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/dailybot_cli.egg-info/top_level.txt +0 -0
  75. {dailybot_cli-3.26.1 → dailybot_cli-3.27.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dailybot-cli
3
- Version: 3.26.1
3
+ Version: 3.27.0
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
@@ -2565,6 +2565,26 @@ class DailyBotClient:
2565
2565
  """DELETE /v1/plan/views/<uuid>/ — permanent."""
2566
2566
  return self._tasks_write("DELETE", f"views/{_path_segment(view_uuid)}/")
2567
2567
 
2568
+ def list_workspace_views(self) -> Any:
2569
+ """GET /v1/plan/views/workspace/ — personal + shared workspace views (person-only)."""
2570
+ return self._tasks_read("views/workspace/")
2571
+
2572
+ def list_workspace_views_with_etag(self) -> tuple[Any, str | None]:
2573
+ """GET /v1/plan/views/workspace/ plus the ETag a save must send back."""
2574
+ return self._tasks_read_with_etag("views/workspace/")
2575
+
2576
+ def save_workspace_views(self, views: list[Any], *, if_match: str) -> Any:
2577
+ """PUT /v1/plan/views/workspace/ — replaces the caller's WHOLE workspace view array.
2578
+
2579
+ Person-only. `If-Match` is required (412 stale, 428 absent).
2580
+ """
2581
+ return self._tasks_write(
2582
+ "PUT",
2583
+ "views/workspace/",
2584
+ json=views,
2585
+ headers={"If-Match": self._checked_if_match(if_match)},
2586
+ )
2587
+
2568
2588
  def list_board_mentionables(self, board_uuid: str) -> Any:
2569
2589
  """GET /v1/plan/boards/<uuid>/mentionables/ — who this viewer may @mention."""
2570
2590
  return self._tasks_read(f"boards/{_path_segment(board_uuid)}/mentionables/")
@@ -2981,6 +3001,78 @@ class DailyBotClient:
2981
3001
  "POST", f"boards/{_path_segment(board_uuid)}/states/reorder/", json={"order": order}
2982
3002
  )
2983
3003
 
3004
+ def reorder_board(
3005
+ self,
3006
+ board_uuid: str,
3007
+ *,
3008
+ before: str | None = None,
3009
+ after: str | None = None,
3010
+ project: str | None = None,
3011
+ ) -> Any:
3012
+ """POST /v1/plan/boards/<uuid>/reorder/ — move among siblings or into another project.
3013
+
3014
+ Name ONE neighbour (`before` / `after`); both is `400 reorder_anchor_invalid`.
3015
+ Neither moves the board last. `project` different from the current one MOVES
3016
+ the board (milestones of tasks that belong to the old project are cleared).
3017
+ """
3018
+ payload: dict[str, Any] = {}
3019
+ if before is not None:
3020
+ payload["before"] = before
3021
+ if after is not None:
3022
+ payload["after"] = after
3023
+ if project is not None:
3024
+ payload["project"] = project
3025
+ return self._tasks_write(
3026
+ "POST", f"boards/{_path_segment(board_uuid)}/reorder/", json=payload
3027
+ )
3028
+
3029
+ def board_move_preview(self, board_uuid: str, *, project: str) -> dict[str, Any]:
3030
+ """GET /v1/plan/boards/<uuid>/move-preview/?project= — what a move would change.
3031
+
3032
+ Writes nothing. `project` is required.
3033
+ """
3034
+ return self._tasks_read(
3035
+ f"boards/{_path_segment(board_uuid)}/move-preview/",
3036
+ params={"project": project},
3037
+ )
3038
+
3039
+ def update_board_member(self, board_uuid: str, user_uuid: str) -> Any:
3040
+ """PATCH …/members/<user>/ — inspect the grant row (role is read-only).
3041
+
3042
+ An empty body returns the current membership. Sending `role` is `400`.
3043
+ """
3044
+ return self._tasks_write(
3045
+ "PATCH",
3046
+ f"boards/{_path_segment(board_uuid)}/members/{_path_segment(user_uuid)}/",
3047
+ json={},
3048
+ )
3049
+
3050
+ def create_board_view(
3051
+ self,
3052
+ board_uuid: str,
3053
+ *,
3054
+ name: str,
3055
+ filters: dict[str, Any] | None = None,
3056
+ idempotency_key: str | None = None,
3057
+ **fields: Any,
3058
+ ) -> Any:
3059
+ """POST /v1/plan/boards/<uuid>/views/ — create one saved view (no If-Match).
3060
+
3061
+ Accepts an Idempotency-Key. Default visibility is personal.
3062
+ """
3063
+ payload: dict[str, Any] = {
3064
+ "name": name,
3065
+ "filters": filters if filters is not None else {},
3066
+ **{k: v for k, v in fields.items() if v is not None},
3067
+ }
3068
+ return self._tasks_write(
3069
+ "POST",
3070
+ f"boards/{_path_segment(board_uuid)}/views/",
3071
+ json=payload,
3072
+ idempotent=True,
3073
+ idempotency_key=idempotency_key,
3074
+ )
3075
+
2984
3076
  def add_board_member(
2985
3077
  self,
2986
3078
  board_uuid: str,
@@ -3021,6 +3113,29 @@ class DailyBotClient:
3021
3113
  "POST", f"boards/{_path_segment(board_uuid)}/labels/", json=payload
3022
3114
  )
3023
3115
 
3116
+ def list_plan_labels(
3117
+ self,
3118
+ *,
3119
+ search: str | None = None,
3120
+ include_archived: bool = False,
3121
+ **page: Any,
3122
+ ) -> PaginatedResult:
3123
+ """GET /v1/plan/labels/ — organization labels under the Plan taxonomy."""
3124
+ params: dict[str, Any] = {}
3125
+ if search:
3126
+ params["search"] = search
3127
+ if include_archived:
3128
+ params["include_archived"] = "true"
3129
+ return self._tasks_list("labels/", params=params or None, **page)
3130
+
3131
+ def create_plan_label(self, *, name: str, **fields: Any) -> Any:
3132
+ """POST /v1/plan/labels/ — create an organization label (Plan surface)."""
3133
+ payload: dict[str, Any] = {
3134
+ "name": name,
3135
+ **{k: v for k, v in fields.items() if v is not None},
3136
+ }
3137
+ return self._tasks_write("POST", "labels/", json=payload)
3138
+
3024
3139
  def update_tasks_label(self, label_uuid: str, **fields: Any) -> dict[str, Any]:
3025
3140
  """PATCH /v1/plan/labels/<uuid>/ — name, color, description, is_archived."""
3026
3141
  payload: dict[str, Any] = {k: v for k, v in fields.items() if v is not None}
@@ -3132,6 +3247,14 @@ class DailyBotClient:
3132
3247
  """GET /v1/plan/tasks/ — strict about parameters; only declared ones."""
3133
3248
  return self._tasks_list("tasks/", params=filters, **page)
3134
3249
 
3250
+ def get_tasks_board(self, *, filters: dict[str, Any] | None = None) -> dict[str, Any]:
3251
+ """GET /v1/plan/tasks/board/ — grouped (optionally swimlaned) snapshot over a slice.
3252
+
3253
+ Flat-list filters apply; `group_by` / `sub_group_by` / `tasks_per_group` /
3254
+ `offset` / `group` / `lane` control the grid. Not a paginated envelope.
3255
+ """
3256
+ return self._tasks_read("tasks/board/", params=filters)
3257
+
3135
3258
  def get_task(self, task_uuid: str) -> dict[str, Any]:
3136
3259
  """GET /v1/plan/tasks/<uuid>/ — the most frequent call of all."""
3137
3260
  return self._tasks_read(f"tasks/{_path_segment(task_uuid)}/")
@@ -4223,6 +4346,61 @@ class DailyBotClient:
4223
4346
  "DELETE", f"projects/{_path_segment(project_uuid)}/members/{_path_segment(user_uuid)}/"
4224
4347
  )
4225
4348
 
4349
+ def update_project_member(self, project_uuid: str, user_uuid: str) -> Any:
4350
+ """PATCH …/members/<user>/ — inspect the grant row (role is read-only).
4351
+
4352
+ An empty body returns the current membership. Sending `role` is `400`.
4353
+ """
4354
+ return self._tasks_write(
4355
+ "PATCH",
4356
+ f"projects/{_path_segment(project_uuid)}/members/{_path_segment(user_uuid)}/",
4357
+ json={},
4358
+ )
4359
+
4360
+ def create_project_view(
4361
+ self,
4362
+ project_uuid: str,
4363
+ *,
4364
+ name: str,
4365
+ filters: dict[str, Any] | None = None,
4366
+ idempotency_key: str | None = None,
4367
+ **fields: Any,
4368
+ ) -> Any:
4369
+ """POST /v1/plan/projects/<uuid>/views/ — create one saved view (no If-Match)."""
4370
+ payload: dict[str, Any] = {
4371
+ "name": name,
4372
+ "filters": filters if filters is not None else {},
4373
+ **{k: v for k, v in fields.items() if v is not None},
4374
+ }
4375
+ return self._tasks_write(
4376
+ "POST",
4377
+ f"projects/{_path_segment(project_uuid)}/views/",
4378
+ json=payload,
4379
+ idempotent=True,
4380
+ idempotency_key=idempotency_key,
4381
+ )
4382
+
4383
+ def reorder_project(
4384
+ self,
4385
+ project_uuid: str,
4386
+ *,
4387
+ before: str | None = None,
4388
+ after: str | None = None,
4389
+ ) -> Any:
4390
+ """POST /v1/plan/projects/<uuid>/reorder/ — move in the shared workspace order.
4391
+
4392
+ Name ONE neighbour; both is `400 reorder_anchor_invalid`. Neither moves last.
4393
+ Org-admin scope (`tasks:admin`).
4394
+ """
4395
+ payload: dict[str, Any] = {}
4396
+ if before is not None:
4397
+ payload["before"] = before
4398
+ if after is not None:
4399
+ payload["after"] = after
4400
+ return self._tasks_write(
4401
+ "POST", f"projects/{_path_segment(project_uuid)}/reorder/", json=payload
4402
+ )
4403
+
4226
4404
  def list_project_views_with_etag(self, project_uuid: str) -> tuple[Any, str | None]:
4227
4405
  """GET /v1/plan/projects/<uuid>/views/ plus the `ETag` a save must send back."""
4228
4406
  return self._tasks_read_with_etag(f"projects/{_path_segment(project_uuid)}/views/")
@@ -214,6 +214,19 @@ _MEMBER_COLUMNS: list[tuple[str, str, bool]] = [
214
214
  ("Role", "role", True),
215
215
  ("User UUID", "user.uuid", True),
216
216
  ]
217
+ _MEMBER_DETAIL_FIELDS: list[tuple[str, str]] = [
218
+ ("UUID", "uuid"),
219
+ ("User UUID", "user_uuid"),
220
+ ("Team UUID", "team_uuid"),
221
+ ("Role", "role"),
222
+ ]
223
+ _MOVE_PREVIEW_FIELDS: list[tuple[str, str]] = [
224
+ ("From project", "from_project_uuid"),
225
+ ("To project", "to_project_uuid"),
226
+ ("Tasks", "tasks_count"),
227
+ ("Foreign milestones", "tasks_with_foreign_milestone"),
228
+ ("Goals affected", "goals_affected"),
229
+ ]
217
230
  _LABEL_COLUMNS: list[tuple[str, str, bool]] = [
218
231
  ("Name", "name", False),
219
232
  ("Color", "color", True),
@@ -789,6 +802,34 @@ def board_member_remove(
789
802
  print_success("Member removed.")
790
803
 
791
804
 
805
+ @board_member.command("update")
806
+ @click.argument("board_uuid", metavar="BOARD")
807
+ @click.argument("user_uuid", metavar="USER")
808
+ @click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
809
+ def board_member_update(board_uuid: str, user_uuid: str, json_mode: bool) -> None:
810
+ """Inspect a board membership grant. Role is read-only (sending one is refused).
811
+
812
+ \b
813
+ An empty PATCH returns the current grant row. Board membership has no role
814
+ column — org roles plus board visibility are the access model.
815
+
816
+ \b
817
+ Examples:
818
+ dailybot plan board member update <board-uuid> <user-uuid>
819
+ dailybot plan board member update <board-uuid> <user-uuid> --json
820
+ """
821
+ client = require_auth()
822
+ try:
823
+ with console.status("Reading the membership..."):
824
+ data: dict[str, Any] = client.update_board_member(board_uuid, user_uuid)
825
+ except APIError as exc:
826
+ exit_for_tasks_error(exc, json_mode)
827
+ if json_mode:
828
+ emit_json(data)
829
+ return
830
+ print_tasks_detail_panel("Board member", data, _MEMBER_DETAIL_FIELDS)
831
+
832
+
792
833
  # ---------------------------------------------------------------------------
793
834
  # Labels and saved views — person-only writes
794
835
  # ---------------------------------------------------------------------------
@@ -935,6 +976,94 @@ def board_view() -> None:
935
976
  """
936
977
 
937
978
 
979
+ @board_view.command("create")
980
+ @click.argument("board_uuid", metavar="BOARD")
981
+ @click.option("-n", "--name", required=True, help="View name (max 64).")
982
+ @click.option(
983
+ "--view-mode",
984
+ default=None,
985
+ type=click.Choice(["list", "board", "kanban", "timeline", "calendar"], case_sensitive=False),
986
+ help="How the filtered set is drawn. `kanban` is accepted as an alias for `board`.",
987
+ )
988
+ @click.option("--group-by", default=None, help="Column dimension (state, owner, priority, …).")
989
+ @click.option("--sub-group-by", default=None, help="Swimlane dimension, or `none`.")
990
+ @click.option("--sort", default=None, help="Sort expression as the web app saves it.")
991
+ @click.option(
992
+ "--visibility",
993
+ default=None,
994
+ type=click.Choice(["personal", "shared", "board_default"], case_sensitive=False),
995
+ help="Default personal. shared/board_default need tasks:admin.",
996
+ )
997
+ @click.option(
998
+ "-f",
999
+ "--filters-file",
1000
+ type=click.File("r"),
1001
+ default=None,
1002
+ help="JSON object of filters (`-` reads stdin). Default empty object.",
1003
+ )
1004
+ @click.option("--idempotency-key", default=None, help="Reuse a key to make a retry safe.")
1005
+ @click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
1006
+ def board_view_create(
1007
+ board_uuid: str,
1008
+ name: str,
1009
+ view_mode: str | None,
1010
+ group_by: str | None,
1011
+ sub_group_by: str | None,
1012
+ sort: str | None,
1013
+ visibility: str | None,
1014
+ filters_file: Any,
1015
+ idempotency_key: str | None,
1016
+ json_mode: bool,
1017
+ ) -> None:
1018
+ """Create one saved view on a board without replacing the whole list.
1019
+
1020
+ \b
1021
+ No If-Match is required (unlike `view save`). Send --idempotency-key so a
1022
+ retry cannot create a second view.
1023
+
1024
+ \b
1025
+ Examples:
1026
+ dailybot plan board view create <board-uuid> -n "My backlog" --view-mode board
1027
+ dailybot plan board view create <board-uuid> -n Blocked -f filters.json --json
1028
+ """
1029
+ filters: dict[str, Any] = {}
1030
+ if filters_file is not None:
1031
+ try:
1032
+ loaded: Any = load_json_input(filters_file)
1033
+ except ValueError as exc:
1034
+ raise click.BadParameter(f"not valid JSON: {exc}", param_hint="--filters-file") from exc
1035
+ if not isinstance(loaded, dict):
1036
+ raise click.BadParameter("must be a JSON object.", param_hint="--filters-file")
1037
+ filters = loaded
1038
+ fields: dict[str, Any] = {
1039
+ k: v
1040
+ for k, v in (
1041
+ ("view_mode", view_mode),
1042
+ ("group_by", group_by),
1043
+ ("sub_group_by", sub_group_by),
1044
+ ("sort", sort),
1045
+ ("visibility", visibility),
1046
+ )
1047
+ if v is not None
1048
+ }
1049
+ client = require_auth()
1050
+ try:
1051
+ with console.status("Creating the view..."):
1052
+ data: dict[str, Any] = client.create_board_view(
1053
+ board_uuid,
1054
+ name=name,
1055
+ filters=filters,
1056
+ idempotency_key=idempotency_key,
1057
+ **fields,
1058
+ )
1059
+ except APIError as exc:
1060
+ exit_for_tasks_error(exc, json_mode)
1061
+ if json_mode:
1062
+ emit_json(data)
1063
+ return
1064
+ report_write(data, f"Created view {named(data, name)}")
1065
+
1066
+
938
1067
  @board_view.command("save")
939
1068
  @click.argument("board_uuid", metavar="BOARD")
940
1069
  @click.option(
@@ -1020,6 +1149,80 @@ def board_view_save(
1020
1149
  print_tasks_rows("Views", rows_of(data), _VIEW_COLUMNS, empty="No saved views.")
1021
1150
 
1022
1151
 
1152
+ @board.command("reorder")
1153
+ @click.argument("board_uuid", metavar="BOARD")
1154
+ @click.option(
1155
+ "--before", default=None, help="Place this board immediately before this sibling board uuid."
1156
+ )
1157
+ @click.option(
1158
+ "--after", default=None, help="Place this board immediately after this sibling board uuid."
1159
+ )
1160
+ @click.option(
1161
+ "--project",
1162
+ default=None,
1163
+ help="Target project uuid. When different from the current one, the board is MOVED there.",
1164
+ )
1165
+ @click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
1166
+ def board_reorder(
1167
+ board_uuid: str,
1168
+ before: str | None,
1169
+ after: str | None,
1170
+ project: str | None,
1171
+ json_mode: bool,
1172
+ ) -> None:
1173
+ """Move a board among siblings, or into another project. Org admin / tasks:admin.
1174
+
1175
+ \b
1176
+ Pass exactly one of --before or --after (or neither to move last). Both is
1177
+ refused (`reorder_anchor_invalid`). Moving to another project clears milestones
1178
+ on tasks that belonged only to the old project; the board key never changes.
1179
+ Preview first with `board move-preview`.
1180
+
1181
+ \b
1182
+ Examples:
1183
+ dailybot plan board reorder <board-uuid> --after <other-board-uuid>
1184
+ dailybot plan board reorder <board-uuid> --project <project-uuid> --before <board-uuid>
1185
+ """
1186
+ if before is not None and after is not None:
1187
+ raise click.UsageError("Pass at most one of --before or --after.")
1188
+ client = require_auth()
1189
+ try:
1190
+ with console.status("Reordering the board..."):
1191
+ data: dict[str, Any] = client.reorder_board(
1192
+ board_uuid, before=before, after=after, project=project
1193
+ )
1194
+ except APIError as exc:
1195
+ exit_for_tasks_error(exc, json_mode)
1196
+ if json_mode:
1197
+ emit_json(data)
1198
+ return
1199
+ report_write(data, f"Reordered board {named(data, board_uuid)}")
1200
+
1201
+
1202
+ @board.command("move-preview")
1203
+ @click.argument("board_uuid", metavar="BOARD")
1204
+ @click.option("--project", required=True, help="Target project uuid to preview a move into.")
1205
+ @click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
1206
+ def board_move_preview(board_uuid: str, project: str, json_mode: bool) -> None:
1207
+ """Show what moving this board into another project would change. Writes nothing.
1208
+
1209
+ \b
1210
+ Examples:
1211
+ dailybot plan board move-preview <board-uuid> --project <project-uuid>
1212
+ dailybot plan board move-preview <board-uuid> --project <project-uuid> --json
1213
+ """
1214
+ client = require_auth()
1215
+ try:
1216
+ with console.status("Building the move preview..."):
1217
+ data: dict[str, Any] = client.board_move_preview(board_uuid, project=project)
1218
+ except APIError as exc:
1219
+ exit_for_tasks_error(exc, json_mode)
1220
+ if json_mode:
1221
+ emit_json(data)
1222
+ return
1223
+ print_tasks_detail_panel("Board move preview", data, _MOVE_PREVIEW_FIELDS)
1224
+
1225
+
1023
1226
  @board.command("snapshot")
1024
1227
  @click.argument("board_uuid", metavar="BOARD")
1025
1228
  @click.option("--sort", default=None, callback=parse_sort, help=SORT_HELP)
@@ -1,13 +1,16 @@
1
1
  """`dailybot plan`: the one root of the Dailybot Plan command groups.
2
2
 
3
3
  Dailybot Plan (public API root ``/v1/plan/``). Its groups (``tasks``, ``task``, ``board``,
4
- ``project``, ``goal``) are reachable only under this root, e.g. ``dailybot plan tasks routes list``.
4
+ ``project``, ``goal``, ``label``, ``views``) are reachable only under this root,
5
+ e.g. ``dailybot plan tasks routes list``.
5
6
  """
6
7
 
7
8
  import click
8
9
 
9
10
  from dailybot_cli.commands.board import board
10
11
  from dailybot_cli.commands.goal import goal
12
+ from dailybot_cli.commands.plan_label import label
13
+ from dailybot_cli.commands.plan_views import views
11
14
  from dailybot_cli.commands.project import project
12
15
  from dailybot_cli.commands.task import task
13
16
  from dailybot_cli.commands.tasks import tasks
@@ -21,11 +24,11 @@ def plan() -> None:
21
24
  Every command lives under `dailybot plan`:
22
25
  dailybot plan tasks status
23
26
  dailybot plan task get ENG-142
24
- dailybot plan board | project | goal ...
27
+ dailybot plan board | project | goal | label | views ...
25
28
  Notifications, routes, reports and the briefing live under the tasks group:
26
29
  dailybot plan tasks notifications get
27
30
  """
28
31
 
29
32
 
30
- for _group in (tasks, task, board, project, goal):
33
+ for _group in (tasks, task, board, project, goal, label, views):
31
34
  plan.add_command(_group)