dailybot-cli 3.20.0__tar.gz → 3.21.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 (65) hide show
  1. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/PKG-INFO +4 -1
  2. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/README.md +3 -0
  3. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/api_client.py +147 -0
  4. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/_attachments.py +32 -3
  5. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/project.py +476 -10
  6. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/public_api_helpers.py +5 -0
  7. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/display.py +37 -0
  8. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli.egg-info/PKG-INFO +4 -1
  9. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/pyproject.toml +1 -1
  10. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/LICENSE +0 -0
  11. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/__init__.py +0 -0
  12. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/__init__.py +0 -0
  13. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/_beta.py +0 -0
  14. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/_briefing.py +0 -0
  15. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/_destructive.py +0 -0
  16. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/_favorites.py +0 -0
  17. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/_rollups.py +0 -0
  18. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/_writes.py +0 -0
  19. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/agent.py +0 -0
  20. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/ask.py +0 -0
  21. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/auth.py +0 -0
  22. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/authoring_helpers.py +0 -0
  23. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/board.py +0 -0
  24. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/channels.py +0 -0
  25. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/chat.py +0 -0
  26. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/checkin.py +0 -0
  27. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/config.py +0 -0
  28. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/conversation.py +0 -0
  29. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/env.py +0 -0
  30. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/featured.py +0 -0
  31. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/form.py +0 -0
  32. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/goal.py +0 -0
  33. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/hook.py +0 -0
  34. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/identity.py +0 -0
  35. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/interactive.py +0 -0
  36. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/interactive_chat.py +0 -0
  37. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/kudos.py +0 -0
  38. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/label.py +0 -0
  39. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/query_options.py +0 -0
  40. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/status.py +0 -0
  41. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/task.py +0 -0
  42. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/tasks.py +0 -0
  43. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/team.py +0 -0
  44. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/uninstall.py +0 -0
  45. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/update.py +0 -0
  46. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/upgrade.py +0 -0
  47. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/user.py +0 -0
  48. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/user_scoped_actions.py +0 -0
  49. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/version.py +0 -0
  50. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/commands/workflow.py +0 -0
  51. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/config.py +0 -0
  52. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/install_method.py +0 -0
  53. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/ledger.py +0 -0
  54. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/main.py +0 -0
  55. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/tui/__init__.py +0 -0
  56. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/tui/app.py +0 -0
  57. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/tui/commands.py +0 -0
  58. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/tui/conversation.py +0 -0
  59. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli/tui/intents.py +0 -0
  60. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli.egg-info/SOURCES.txt +0 -0
  61. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli.egg-info/dependency_links.txt +0 -0
  62. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli.egg-info/entry_points.txt +0 -0
  63. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli.egg-info/requires.txt +0 -0
  64. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/dailybot_cli.egg-info/top_level.txt +0 -0
  65. {dailybot_cli-3.20.0 → dailybot_cli-3.21.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dailybot-cli
3
- Version: 3.20.0
3
+ Version: 3.21.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
@@ -992,6 +992,8 @@ humans who own them.
992
992
  | `dailybot project get <uuid>` | Show one project |
993
993
  | `dailybot project updates [<project>]` | Batched update digest, or one project's feed |
994
994
  | `dailybot project update-post <uuid> <body>` | **Post a project update** — how the team sees what was done (`--health`; sends an idempotency key) |
995
+ | `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
+ | `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>` |
995
997
  | `dailybot project milestones [<uuid>]` | List milestones |
996
998
  | `dailybot project milestone-complete <p> <m>` | Complete a milestone. **Its open tasks stay open** |
997
999
  | `dailybot project milestone-reopen <p> <m>` | Reopen a milestone |
@@ -1001,6 +1003,7 @@ humans who own them.
1001
1003
  | `dailybot project members <uuid>` · `project member add\|remove` | Who can see the project — people or whole teams (`--team`) — `members` works with a personal API key; `member add\|remove` **needs a person: `dailybot login` or a personal API key** |
1002
1004
  | `dailybot project views <uuid>` · `project view save` | Your saved views; save replaces the list and requires the ETag — **needs a person: `dailybot login` or a personal API key** |
1003
1005
  | `dailybot project milestone-create\|milestone-update\|milestone-delete` | Dated milestones; delete retires it (tasks keep pointing at it) |
1006
+ | `dailybot project milestone-attach\|milestone-attachments <project> <milestone>` · `milestone-attachment get\|rename\|delete` | Files on a milestone (≤5 MiB); reference one in the description with `attachment:<uuid>`. `project milestone-restore` brings a retired milestone back |
1004
1007
  | `dailybot project archive <uuid>` | Archive a project |
1005
1008
  | `dailybot project attach <uuid> <file>` · `project attachments` · `project attachment get\|delete` | Files on a project (≤5 MiB, one request). Attaching and deleting need a person (`dailybot login` or a personal API key) |
1006
1009
  | `dailybot goal list` | List goals (`--include` is repeatable: `--include progress --include projects`) |
@@ -953,6 +953,8 @@ humans who own them.
953
953
  | `dailybot project get <uuid>` | Show one project |
954
954
  | `dailybot project updates [<project>]` | Batched update digest, or one project's feed |
955
955
  | `dailybot project update-post <uuid> <body>` | **Post a project update** — how the team sees what was done (`--health`; sends an idempotency key) |
956
+ | `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`) |
957
+ | `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>` |
956
958
  | `dailybot project milestones [<uuid>]` | List milestones |
957
959
  | `dailybot project milestone-complete <p> <m>` | Complete a milestone. **Its open tasks stay open** |
958
960
  | `dailybot project milestone-reopen <p> <m>` | Reopen a milestone |
@@ -962,6 +964,7 @@ humans who own them.
962
964
  | `dailybot project members <uuid>` · `project member add\|remove` | Who can see the project — people or whole teams (`--team`) — `members` works with a personal API key; `member add\|remove` **needs a person: `dailybot login` or a personal API key** |
963
965
  | `dailybot project views <uuid>` · `project view save` | Your saved views; save replaces the list and requires the ETag — **needs a person: `dailybot login` or a personal API key** |
964
966
  | `dailybot project milestone-create\|milestone-update\|milestone-delete` | Dated milestones; delete retires it (tasks keep pointing at it) |
967
+ | `dailybot project milestone-attach\|milestone-attachments <project> <milestone>` · `milestone-attachment get\|rename\|delete` | Files on a milestone (≤5 MiB); reference one in the description with `attachment:<uuid>`. `project milestone-restore` brings a retired milestone back |
965
968
  | `dailybot project archive <uuid>` | Archive a project |
966
969
  | `dailybot project attach <uuid> <file>` · `project attachments` · `project attachment get\|delete` | Files on a project (≤5 MiB, one request). Attaching and deleting need a person (`dailybot login` or a personal API key) |
967
970
  | `dailybot goal list` | List goals (`--include` is repeatable: `--include progress --include projects`) |
@@ -100,6 +100,8 @@ SAME_ORIGIN_UPLOAD_METHODS: frozenset[str] = frozenset({"PUT", "POST"})
100
100
  # BLAST_RADIUS.md records this as THE volume guard for unattended destructive
101
101
  # loops — the CLI adds no second ceiling of its own.
102
102
  TASKS_BULK_MAX_ITEMS: int = 100
103
+ # Body field an attachment rename (PATCH …/attachments/<uuid>/) carries.
104
+ ATTACHMENT_RENAME_FIELD: str = "filename"
103
105
  # Collections the task detail door can embed in one read (first page each).
104
106
  TASK_BRIEFING_INCLUDES: tuple[str, ...] = (
105
107
  "relations",
@@ -3321,6 +3323,143 @@ class DailyBotClient:
3321
3323
  """GET /v1/tasks/projects/<p>/attachments/<a>/content/."""
3322
3324
  return self._download_attachment_of(self._project_parent(project_uuid), attachment_uuid)
3323
3325
 
3326
+ # Milestone and project-update attachments: the same row shape as project ones.
3327
+ @staticmethod
3328
+ def _milestone_parent(project_uuid: str, milestone_uuid: str) -> str:
3329
+ return f"projects/{_path_segment(project_uuid)}/milestones/{_path_segment(milestone_uuid)}"
3330
+
3331
+ @staticmethod
3332
+ def _update_parent(project_uuid: str, update_uuid: str) -> str:
3333
+ return f"projects/{_path_segment(project_uuid)}/updates/{_path_segment(update_uuid)}"
3334
+
3335
+ def _rename_attachment_of(self, parent: str, attachment_uuid: str, *, filename: str) -> Any:
3336
+ """PATCH <parent>/attachments/<uuid>/ — rename the file."""
3337
+ return self._tasks_write(
3338
+ "PATCH",
3339
+ f"{parent}/attachments/{_path_segment(attachment_uuid)}/",
3340
+ json={ATTACHMENT_RENAME_FIELD: filename},
3341
+ )
3342
+
3343
+ def upload_milestone_attachment(
3344
+ self,
3345
+ project_uuid: str,
3346
+ milestone_uuid: str,
3347
+ *,
3348
+ filename: str,
3349
+ content_type: str,
3350
+ data: bytes,
3351
+ caption: str | None = None,
3352
+ ) -> dict[str, Any]:
3353
+ """POST …/milestones/<m>/attachments/ as multipart (≤5 MiB)."""
3354
+ return self._upload_attachment_to(
3355
+ self._milestone_parent(project_uuid, milestone_uuid),
3356
+ filename=filename,
3357
+ content_type=content_type,
3358
+ data=data,
3359
+ caption=caption,
3360
+ )
3361
+
3362
+ def list_milestone_attachments(self, project_uuid: str, milestone_uuid: str) -> Any:
3363
+ """GET …/milestones/<m>/attachments/."""
3364
+ return self._list_attachments_of(self._milestone_parent(project_uuid, milestone_uuid))
3365
+
3366
+ def download_milestone_attachment(
3367
+ self, project_uuid: str, milestone_uuid: str, attachment_uuid: str
3368
+ ) -> bytes:
3369
+ """GET …/milestones/<m>/attachments/<a>/content/."""
3370
+ return self._download_attachment_of(
3371
+ self._milestone_parent(project_uuid, milestone_uuid), attachment_uuid
3372
+ )
3373
+
3374
+ def rename_milestone_attachment(
3375
+ self, project_uuid: str, milestone_uuid: str, attachment_uuid: str, *, filename: str
3376
+ ) -> Any:
3377
+ """PATCH …/milestones/<m>/attachments/<a>/."""
3378
+ return self._rename_attachment_of(
3379
+ self._milestone_parent(project_uuid, milestone_uuid), attachment_uuid, filename=filename
3380
+ )
3381
+
3382
+ def delete_milestone_attachment(
3383
+ self, project_uuid: str, milestone_uuid: str, attachment_uuid: str
3384
+ ) -> Any:
3385
+ """DELETE …/milestones/<m>/attachments/<a>/."""
3386
+ return self._delete_attachment_of(
3387
+ self._milestone_parent(project_uuid, milestone_uuid), attachment_uuid
3388
+ )
3389
+
3390
+ def get_project_update(self, project_uuid: str, update_uuid: str) -> dict[str, Any]:
3391
+ """GET /v1/tasks/projects/<p>/updates/<u>/."""
3392
+ return self._tasks_read(f"{self._update_parent(project_uuid, update_uuid)}/")
3393
+
3394
+ def edit_project_update(
3395
+ self,
3396
+ project_uuid: str,
3397
+ update_uuid: str,
3398
+ *,
3399
+ body: str | None = None,
3400
+ health: str | None = None,
3401
+ ) -> Any:
3402
+ """PATCH /v1/tasks/projects/<p>/updates/<u>/ — author only (`update_not_author`)."""
3403
+ payload: dict[str, Any] = {}
3404
+ if body is not None:
3405
+ payload["body"] = body
3406
+ if health is not None:
3407
+ payload["health"] = health
3408
+ return self._tasks_write(
3409
+ "PATCH", f"{self._update_parent(project_uuid, update_uuid)}/", json=payload
3410
+ )
3411
+
3412
+ def delete_project_update(self, project_uuid: str, update_uuid: str) -> Any:
3413
+ """DELETE /v1/tasks/projects/<p>/updates/<u>/ — the author or an org admin."""
3414
+ return self._tasks_write("DELETE", f"{self._update_parent(project_uuid, update_uuid)}/")
3415
+
3416
+ def upload_update_attachment(
3417
+ self,
3418
+ project_uuid: str,
3419
+ update_uuid: str,
3420
+ *,
3421
+ filename: str,
3422
+ content_type: str,
3423
+ data: bytes,
3424
+ caption: str | None = None,
3425
+ ) -> dict[str, Any]:
3426
+ """POST …/updates/<u>/attachments/ as multipart (≤5 MiB) — author only."""
3427
+ return self._upload_attachment_to(
3428
+ self._update_parent(project_uuid, update_uuid),
3429
+ filename=filename,
3430
+ content_type=content_type,
3431
+ data=data,
3432
+ caption=caption,
3433
+ )
3434
+
3435
+ def list_update_attachments(self, project_uuid: str, update_uuid: str) -> Any:
3436
+ """GET …/updates/<u>/attachments/."""
3437
+ return self._list_attachments_of(self._update_parent(project_uuid, update_uuid))
3438
+
3439
+ def download_update_attachment(
3440
+ self, project_uuid: str, update_uuid: str, attachment_uuid: str
3441
+ ) -> bytes:
3442
+ """GET …/updates/<u>/attachments/<a>/content/."""
3443
+ return self._download_attachment_of(
3444
+ self._update_parent(project_uuid, update_uuid), attachment_uuid
3445
+ )
3446
+
3447
+ def rename_update_attachment(
3448
+ self, project_uuid: str, update_uuid: str, attachment_uuid: str, *, filename: str
3449
+ ) -> Any:
3450
+ """PATCH …/updates/<u>/attachments/<a>/."""
3451
+ return self._rename_attachment_of(
3452
+ self._update_parent(project_uuid, update_uuid), attachment_uuid, filename=filename
3453
+ )
3454
+
3455
+ def delete_update_attachment(
3456
+ self, project_uuid: str, update_uuid: str, attachment_uuid: str
3457
+ ) -> Any:
3458
+ """DELETE …/updates/<u>/attachments/<a>/."""
3459
+ return self._delete_attachment_of(
3460
+ self._update_parent(project_uuid, update_uuid), attachment_uuid
3461
+ )
3462
+
3324
3463
  # Goal attachments (POST / DELETE: signed-in person; reads: anyone who can see it)
3325
3464
  def upload_goal_attachment(
3326
3465
  self,
@@ -3599,6 +3738,14 @@ class DailyBotClient:
3599
3738
  json={k: v for k, v in fields.items() if v is not None},
3600
3739
  )
3601
3740
 
3741
+ def restore_milestone(self, project_uuid: str, milestone_uuid: str) -> dict[str, Any]:
3742
+ """POST …/milestones/<uuid>/restore/ — bring a retired milestone back (idempotent)."""
3743
+ return self._tasks_write(
3744
+ "POST",
3745
+ f"projects/{_path_segment(project_uuid)}/milestones/{_path_segment(milestone_uuid)}/"
3746
+ "restore/",
3747
+ )
3748
+
3602
3749
  def delete_milestone(self, project_uuid: str, milestone_uuid: str) -> Any:
3603
3750
  """DELETE …/milestones/<uuid>/ — retires (archives); tasks keep pointing at it."""
3604
3751
  return self._tasks_write(
@@ -1,9 +1,10 @@
1
- """Shared attachment flows for tasks, comments, projects and goals.
1
+ """Shared attachment flows for tasks, comments, projects, goals, milestones and updates.
2
2
 
3
- Four parents own an `attachments/` collection with the same row shape, so the
3
+ Six parents own an `attachments/` collection with the same row shape, so the
4
4
  commands share one implementation of each step: read the local file under a
5
5
  hard limit, upload, list, download to a path that is never overwritten by
6
- surprise, and delete with a stated consequence. Each command supplies only the
6
+ surprise, rename within the server's bounds, and delete with a stated
7
+ consequence. Each command supplies only the
7
8
  client call for its parent.
8
9
  """
9
10
 
@@ -190,6 +191,34 @@ def run_get(
190
191
  print_success(f"Saved {len(content)} bytes to {output}.")
191
192
 
192
193
 
194
+ ATTACHMENT_NAME_MAX_CHARS: int = 255
195
+
196
+
197
+ def run_rename(
198
+ rename: Callable[[Any, str], Any],
199
+ filename: str,
200
+ *,
201
+ json_mode: bool,
202
+ require_auth: Callable[[], Any],
203
+ ) -> None:
204
+ """Rename one attachment. The name is checked locally against the server's bounds."""
205
+ cleaned: str = filename.strip()
206
+ if not cleaned or len(cleaned) > ATTACHMENT_NAME_MAX_CHARS:
207
+ raise click.UsageError(
208
+ f"The new name must be 1 to {ATTACHMENT_NAME_MAX_CHARS} characters. Nothing was sent."
209
+ )
210
+ client: Any = require_auth()
211
+ try:
212
+ with console.status("Renaming the attachment..."):
213
+ data: Any = rename(client, cleaned)
214
+ except APIError as exc:
215
+ exit_for_tasks_error(exc, json_mode)
216
+ if json_mode:
217
+ emit_json(data)
218
+ return
219
+ print_success(f"Attachment renamed to {cleaned}.")
220
+
221
+
193
222
  def run_delete(
194
223
  delete: Callable[[Any], Any],
195
224
  consequence: str,
@@ -7,7 +7,13 @@ from typing import Any
7
7
  import click
8
8
 
9
9
  from dailybot_cli.api_client import ATTACHMENT_MULTIPART_MAX_BYTES, APIError, PaginatedResult
10
- from dailybot_cli.commands._attachments import run_attach, run_delete, run_get, run_list
10
+ from dailybot_cli.commands._attachments import (
11
+ run_attach,
12
+ run_delete,
13
+ run_get,
14
+ run_list,
15
+ run_rename,
16
+ )
11
17
  from dailybot_cli.commands._beta import mark_beta
12
18
  from dailybot_cli.commands._destructive import confirm_without_preview, preview_then_confirm
13
19
  from dailybot_cli.commands._rollups import render_rollup
@@ -22,16 +28,15 @@ from dailybot_cli.commands.public_api_helpers import (
22
28
  from dailybot_cli.commands.query_options import build_query_params, query_options, resolve_fetch_all
23
29
  from dailybot_cli.display import (
24
30
  console,
25
- present_untrusted,
26
31
  print_info,
27
32
  print_milestones_table,
28
33
  print_pagination_footer,
34
+ print_project_updates,
29
35
  print_projects_table,
30
36
  print_raw_value,
31
37
  print_success,
32
38
  print_tasks_detail_panel,
33
39
  print_tasks_rows,
34
- safe_text,
35
40
  )
36
41
 
37
42
  # Split deliberately. `projects` is a goal-shaped selector: a project has no
@@ -193,11 +198,7 @@ def project_updates(project_uuid: str | None, json_mode: bool, **flags: Any) ->
193
198
  if json_mode:
194
199
  emit_json(_envelope(result))
195
200
  return
196
- for update in result.results:
197
- console.print(
198
- f"[dim]{safe_text(update.get('created_at', ''))}[/dim] "
199
- f"{present_untrusted(update.get('body'), limit=160)}"
200
- )
201
+ print_project_updates(result.results)
201
202
  print_pagination_footer(len(result.results), result.count, has_more=bool(result.next))
202
203
 
203
204
 
@@ -928,9 +929,32 @@ def project_milestone_delete(
928
929
  print_success("Milestone retired.")
929
930
 
930
931
 
932
+ @project.command("milestone-restore")
933
+ @click.argument("project_uuid", metavar="PROJECT")
934
+ @click.argument("milestone_uuid", metavar="MILESTONE")
935
+ @click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
936
+ def project_milestone_restore(project_uuid: str, milestone_uuid: str, json_mode: bool) -> None:
937
+ """Bring a retired milestone back. Safe to repeat.
938
+
939
+ \b
940
+ Examples:
941
+ dailybot project milestone-restore <project-uuid> <milestone-uuid>
942
+ """
943
+ client = require_auth()
944
+ try:
945
+ with console.status("Restoring the milestone..."):
946
+ data: dict[str, Any] = client.restore_milestone(project_uuid, milestone_uuid)
947
+ except APIError as exc:
948
+ exit_for_tasks_error(exc, json_mode)
949
+ if json_mode:
950
+ emit_json(data)
951
+ return
952
+ print_success("Milestone restored.")
953
+
954
+
931
955
  # ---------------------------------------------------------------------------
932
- # Attachments. Reading needs only visibility; attaching and deleting are
933
- # Structure doors: refuse an organization API key before any request (keys lack tasks:admin).
956
+ # Attachments. Reading needs only visibility; attaching and deleting need a
957
+ # person (a login or a personal API key) who can change the project.
934
958
  # ---------------------------------------------------------------------------
935
959
 
936
960
 
@@ -1053,3 +1077,445 @@ def project_attachment_delete(
1053
1077
  json_mode=json_mode,
1054
1078
  require_auth=require_auth,
1055
1079
  )
1080
+
1081
+
1082
+ # ---------------------------------------------------------------------------
1083
+ # Milestone attachments. Anyone who can see the project and edit its milestones
1084
+ # (a login or a personal API key) can attach, rename and delete.
1085
+ # ---------------------------------------------------------------------------
1086
+
1087
+ _OUTPUT_OPTION = click.option(
1088
+ "-o",
1089
+ "--output",
1090
+ "output",
1091
+ type=click.Path(dir_okay=False, writable=True, path_type=Path),
1092
+ required=True,
1093
+ help="Where to write the file.",
1094
+ )
1095
+
1096
+
1097
+ @project.command("milestone-attach")
1098
+ @click.argument("project_uuid", metavar="PROJECT")
1099
+ @click.argument("milestone_uuid", metavar="MILESTONE")
1100
+ @click.argument(
1101
+ "file_path",
1102
+ metavar="FILE",
1103
+ type=click.Path(exists=True, dir_okay=False, readable=True, path_type=Path),
1104
+ )
1105
+ @click.option("--caption", default=None, help="Short caption shown with the file.")
1106
+ @click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
1107
+ def project_milestone_attach(
1108
+ project_uuid: str, milestone_uuid: str, file_path: Path, caption: str | None, json_mode: bool
1109
+ ) -> None:
1110
+ """Attach a file to a milestone. Needs a person: `dailybot login` or a personal API key.
1111
+
1112
+ \b
1113
+ One request, up to 5 MiB. Reference it in the milestone description with
1114
+ `attachment:<uuid>` to show it inline.
1115
+
1116
+ \b
1117
+ Examples:
1118
+ dailybot project milestone-attach <project-uuid> <milestone-uuid> ./spec.pdf
1119
+ """
1120
+ run_attach(
1121
+ lambda client, **file: client.upload_milestone_attachment(
1122
+ project_uuid, milestone_uuid, **file
1123
+ ),
1124
+ file_path,
1125
+ caption=caption,
1126
+ limit=ATTACHMENT_MULTIPART_MAX_BYTES,
1127
+ where="per file on a milestone",
1128
+ json_mode=json_mode,
1129
+ require_auth=require_auth,
1130
+ )
1131
+
1132
+
1133
+ @project.command("milestone-attachments")
1134
+ @click.argument("project_uuid", metavar="PROJECT")
1135
+ @click.argument("milestone_uuid", metavar="MILESTONE")
1136
+ @click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
1137
+ def project_milestone_attachments(project_uuid: str, milestone_uuid: str, json_mode: bool) -> None:
1138
+ """List a milestone's attachments.
1139
+
1140
+ \b
1141
+ Examples:
1142
+ dailybot project milestone-attachments <project-uuid> <milestone-uuid> --json
1143
+ """
1144
+ run_list(
1145
+ lambda client: client.list_milestone_attachments(project_uuid, milestone_uuid),
1146
+ json_mode=json_mode,
1147
+ require_auth=require_auth,
1148
+ )
1149
+
1150
+
1151
+ @project.group("milestone-attachment")
1152
+ def project_milestone_attachment() -> None:
1153
+ """Download, rename or delete one attachment on a milestone.
1154
+
1155
+ \b
1156
+ Examples:
1157
+ dailybot project milestone-attachment get <project> <milestone> <attachment> -o ./spec.pdf
1158
+ dailybot project milestone-attachment rename <project> <milestone> <attachment> spec-v2.pdf
1159
+ dailybot project milestone-attachment delete <project> <milestone> <attachment> --dry-run
1160
+ """
1161
+
1162
+
1163
+ @project_milestone_attachment.command("get")
1164
+ @click.argument("project_uuid", metavar="PROJECT")
1165
+ @click.argument("milestone_uuid", metavar="MILESTONE")
1166
+ @click.argument("attachment_uuid", metavar="ATTACHMENT")
1167
+ @_OUTPUT_OPTION
1168
+ @click.option("--force", is_flag=True, help="Overwrite the output file if it exists.")
1169
+ @click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
1170
+ def project_milestone_attachment_get(
1171
+ project_uuid: str,
1172
+ milestone_uuid: str,
1173
+ attachment_uuid: str,
1174
+ output: Path,
1175
+ force: bool,
1176
+ json_mode: bool,
1177
+ ) -> None:
1178
+ """Download a milestone's attachment to a file. Never overwrites without --force.
1179
+
1180
+ \b
1181
+ Examples:
1182
+ dailybot project milestone-attachment get <project> <milestone> <attachment> -o ./spec.pdf
1183
+ """
1184
+ run_get(
1185
+ lambda client: client.download_milestone_attachment(
1186
+ project_uuid, milestone_uuid, attachment_uuid
1187
+ ),
1188
+ output,
1189
+ attachment_uuid=attachment_uuid,
1190
+ force=force,
1191
+ json_mode=json_mode,
1192
+ require_auth=require_auth,
1193
+ )
1194
+
1195
+
1196
+ @project_milestone_attachment.command("rename")
1197
+ @click.argument("project_uuid", metavar="PROJECT")
1198
+ @click.argument("milestone_uuid", metavar="MILESTONE")
1199
+ @click.argument("attachment_uuid", metavar="ATTACHMENT")
1200
+ @click.argument("filename")
1201
+ @click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
1202
+ def project_milestone_attachment_rename(
1203
+ project_uuid: str, milestone_uuid: str, attachment_uuid: str, filename: str, json_mode: bool
1204
+ ) -> None:
1205
+ """Rename a milestone's attachment (1 to 255 characters).
1206
+
1207
+ \b
1208
+ Examples:
1209
+ dailybot project milestone-attachment rename <project> <milestone> <attachment> spec-v2.pdf
1210
+ """
1211
+ run_rename(
1212
+ lambda client, name: client.rename_milestone_attachment(
1213
+ project_uuid, milestone_uuid, attachment_uuid, filename=name
1214
+ ),
1215
+ filename,
1216
+ json_mode=json_mode,
1217
+ require_auth=require_auth,
1218
+ )
1219
+
1220
+
1221
+ @project_milestone_attachment.command("delete")
1222
+ @click.argument("project_uuid", metavar="PROJECT")
1223
+ @click.argument("milestone_uuid", metavar="MILESTONE")
1224
+ @click.argument("attachment_uuid", metavar="ATTACHMENT")
1225
+ @click.option("--dry-run", is_flag=True, help="Say what would happen and send nothing.")
1226
+ @click.option("-y", "--yes", "assume_yes", is_flag=True, help="Skip the confirmation.")
1227
+ @click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
1228
+ def project_milestone_attachment_delete(
1229
+ project_uuid: str,
1230
+ milestone_uuid: str,
1231
+ attachment_uuid: str,
1232
+ dry_run: bool,
1233
+ assume_yes: bool,
1234
+ json_mode: bool,
1235
+ ) -> None:
1236
+ """Remove an attachment from a milestone. This cannot be undone.
1237
+
1238
+ \b
1239
+ Examples:
1240
+ dailybot project milestone-attachment delete <project> <milestone> <attachment> --dry-run
1241
+ dailybot project milestone-attachment delete <project> <milestone> <attachment> --yes
1242
+ """
1243
+ run_delete(
1244
+ lambda client: client.delete_milestone_attachment(
1245
+ project_uuid, milestone_uuid, attachment_uuid
1246
+ ),
1247
+ f"delete attachment {attachment_uuid} from milestone {milestone_uuid}.",
1248
+ receipt={
1249
+ "project": project_uuid,
1250
+ "milestone": milestone_uuid,
1251
+ "attachment": attachment_uuid,
1252
+ },
1253
+ dry_run=dry_run,
1254
+ assume_yes=assume_yes,
1255
+ json_mode=json_mode,
1256
+ require_auth=require_auth,
1257
+ )
1258
+
1259
+
1260
+ # ---------------------------------------------------------------------------
1261
+ # One project update: read, edit (author only), delete (author or org admin),
1262
+ # and its attachments (attach and rename: author only; delete: author or org
1263
+ # admin). `--agent-name` stamps every write.
1264
+ # ---------------------------------------------------------------------------
1265
+
1266
+
1267
+ @project.command("update-get")
1268
+ @click.argument("project_uuid", metavar="PROJECT")
1269
+ @click.argument("update_uuid", metavar="UPDATE")
1270
+ @click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
1271
+ def project_update_get(project_uuid: str, update_uuid: str, json_mode: bool) -> None:
1272
+ """Show one project update, with its author, agent, health and attachments.
1273
+
1274
+ \b
1275
+ Examples:
1276
+ dailybot project update-get <project-uuid> <update-uuid> --json
1277
+ """
1278
+ client = require_auth()
1279
+ try:
1280
+ with console.status("Reading the update..."):
1281
+ data: dict[str, Any] = client.get_project_update(project_uuid, update_uuid)
1282
+ except APIError as exc:
1283
+ exit_for_tasks_error(exc, json_mode)
1284
+ if json_mode:
1285
+ emit_json(data)
1286
+ return
1287
+ print_project_updates([data])
1288
+
1289
+
1290
+ @project.command("update-edit")
1291
+ @click.argument("project_uuid", metavar="PROJECT")
1292
+ @click.argument("update_uuid", metavar="UPDATE")
1293
+ @click.argument("body", required=False)
1294
+ @click.option(
1295
+ "--health",
1296
+ type=click.Choice(PROJECT_HEALTH),
1297
+ default=None,
1298
+ help="Change the health this update claims.",
1299
+ )
1300
+ @click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
1301
+ def project_update_edit(
1302
+ project_uuid: str, update_uuid: str, body: str | None, health: str | None, json_mode: bool
1303
+ ) -> None:
1304
+ """Edit your project update's text and/or health. Only its author can.
1305
+
1306
+ \b
1307
+ Pass `-` as the body to read it from stdin. To show an attached image inline,
1308
+ put `attachment:<uuid>` in the body.
1309
+
1310
+ \b
1311
+ Examples:
1312
+ dailybot project update-edit <project-uuid> <update-uuid> "Shipped, with the fix"
1313
+ dailybot project update-edit <project-uuid> <update-uuid> --health at_risk
1314
+ """
1315
+ if body is None and health is None:
1316
+ raise click.UsageError("Pass a new BODY, --health, or both. Nothing was sent.")
1317
+ client = require_auth()
1318
+ try:
1319
+ with console.status("Editing the update..."):
1320
+ data: Any = client.edit_project_update(
1321
+ project_uuid,
1322
+ update_uuid,
1323
+ body=_read_body(body) if body is not None else None,
1324
+ health=health,
1325
+ )
1326
+ except APIError as exc:
1327
+ exit_for_tasks_error(exc, json_mode)
1328
+ if json_mode:
1329
+ emit_json(data)
1330
+ return
1331
+ report_write(data, "Project update edited")
1332
+
1333
+
1334
+ @project.command("update-delete")
1335
+ @click.argument("project_uuid", metavar="PROJECT")
1336
+ @click.argument("update_uuid", metavar="UPDATE")
1337
+ @click.option("--dry-run", is_flag=True, help="Say what would happen and send nothing.")
1338
+ @click.option("-y", "--yes", "assume_yes", is_flag=True, help="Skip the confirmation.")
1339
+ @click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
1340
+ def project_update_delete(
1341
+ project_uuid: str, update_uuid: str, dry_run: bool, assume_yes: bool, json_mode: bool
1342
+ ) -> None:
1343
+ """Delete a project update. Its author or an organization admin can. Cannot be undone.
1344
+
1345
+ \b
1346
+ Examples:
1347
+ dailybot project update-delete <project-uuid> <update-uuid> --dry-run
1348
+ dailybot project update-delete <project-uuid> <update-uuid> --yes
1349
+ """
1350
+ if not confirm_without_preview(
1351
+ f"delete project update {update_uuid} and its attachments.",
1352
+ assume_yes=assume_yes,
1353
+ dry_run=dry_run,
1354
+ json_mode=json_mode,
1355
+ ):
1356
+ return
1357
+ client = require_auth()
1358
+ try:
1359
+ with console.status("Deleting the update..."):
1360
+ client.delete_project_update(project_uuid, update_uuid)
1361
+ except APIError as exc:
1362
+ exit_for_tasks_error(exc, json_mode)
1363
+ if json_mode:
1364
+ emit_json({"deleted": True, "project": project_uuid, "update": update_uuid})
1365
+ return
1366
+ print_success("Project update deleted.")
1367
+
1368
+
1369
+ @project.command("update-attach")
1370
+ @click.argument("project_uuid", metavar="PROJECT")
1371
+ @click.argument("update_uuid", metavar="UPDATE")
1372
+ @click.argument(
1373
+ "file_path",
1374
+ metavar="FILE",
1375
+ type=click.Path(exists=True, dir_okay=False, readable=True, path_type=Path),
1376
+ )
1377
+ @click.option("--caption", default=None, help="Short caption shown with the file.")
1378
+ @click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
1379
+ def project_update_attach(
1380
+ project_uuid: str, update_uuid: str, file_path: Path, caption: str | None, json_mode: bool
1381
+ ) -> None:
1382
+ """Attach a file to your project update. Only its author can.
1383
+
1384
+ \b
1385
+ One request, up to 5 MiB. For an inline image: post the update, attach the
1386
+ file, then `update-edit` the body with `attachment:<uuid>`.
1387
+
1388
+ \b
1389
+ Examples:
1390
+ dailybot project update-attach <project-uuid> <update-uuid> ./chart.png
1391
+ """
1392
+ run_attach(
1393
+ lambda client, **file: client.upload_update_attachment(project_uuid, update_uuid, **file),
1394
+ file_path,
1395
+ caption=caption,
1396
+ limit=ATTACHMENT_MULTIPART_MAX_BYTES,
1397
+ where="per file on a project update",
1398
+ json_mode=json_mode,
1399
+ require_auth=require_auth,
1400
+ )
1401
+
1402
+
1403
+ @project.command("update-attachments")
1404
+ @click.argument("project_uuid", metavar="PROJECT")
1405
+ @click.argument("update_uuid", metavar="UPDATE")
1406
+ @click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
1407
+ def project_update_attachments(project_uuid: str, update_uuid: str, json_mode: bool) -> None:
1408
+ """List a project update's attachments.
1409
+
1410
+ \b
1411
+ Examples:
1412
+ dailybot project update-attachments <project-uuid> <update-uuid> --json
1413
+ """
1414
+ run_list(
1415
+ lambda client: client.list_update_attachments(project_uuid, update_uuid),
1416
+ json_mode=json_mode,
1417
+ require_auth=require_auth,
1418
+ )
1419
+
1420
+
1421
+ @project.group("update-attachment")
1422
+ def project_update_attachment() -> None:
1423
+ """Download, rename or delete one attachment on a project update.
1424
+
1425
+ \b
1426
+ Examples:
1427
+ dailybot project update-attachment get <project> <update> <attachment> -o ./chart.png
1428
+ dailybot project update-attachment rename <project> <update> <attachment> chart-q4.png
1429
+ dailybot project update-attachment delete <project> <update> <attachment> --yes
1430
+ """
1431
+
1432
+
1433
+ @project_update_attachment.command("get")
1434
+ @click.argument("project_uuid", metavar="PROJECT")
1435
+ @click.argument("update_uuid", metavar="UPDATE")
1436
+ @click.argument("attachment_uuid", metavar="ATTACHMENT")
1437
+ @_OUTPUT_OPTION
1438
+ @click.option("--force", is_flag=True, help="Overwrite the output file if it exists.")
1439
+ @click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
1440
+ def project_update_attachment_get(
1441
+ project_uuid: str,
1442
+ update_uuid: str,
1443
+ attachment_uuid: str,
1444
+ output: Path,
1445
+ force: bool,
1446
+ json_mode: bool,
1447
+ ) -> None:
1448
+ """Download a project update's attachment. Never overwrites without --force.
1449
+
1450
+ \b
1451
+ Examples:
1452
+ dailybot project update-attachment get <project> <update> <attachment> -o ./chart.png
1453
+ """
1454
+ run_get(
1455
+ lambda client: client.download_update_attachment(
1456
+ project_uuid, update_uuid, attachment_uuid
1457
+ ),
1458
+ output,
1459
+ attachment_uuid=attachment_uuid,
1460
+ force=force,
1461
+ json_mode=json_mode,
1462
+ require_auth=require_auth,
1463
+ )
1464
+
1465
+
1466
+ @project_update_attachment.command("rename")
1467
+ @click.argument("project_uuid", metavar="PROJECT")
1468
+ @click.argument("update_uuid", metavar="UPDATE")
1469
+ @click.argument("attachment_uuid", metavar="ATTACHMENT")
1470
+ @click.argument("filename")
1471
+ @click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
1472
+ def project_update_attachment_rename(
1473
+ project_uuid: str, update_uuid: str, attachment_uuid: str, filename: str, json_mode: bool
1474
+ ) -> None:
1475
+ """Rename a project update's attachment (author only; 1 to 255 characters).
1476
+
1477
+ \b
1478
+ Examples:
1479
+ dailybot project update-attachment rename <project> <update> <attachment> chart-q4.png
1480
+ """
1481
+ run_rename(
1482
+ lambda client, name: client.rename_update_attachment(
1483
+ project_uuid, update_uuid, attachment_uuid, filename=name
1484
+ ),
1485
+ filename,
1486
+ json_mode=json_mode,
1487
+ require_auth=require_auth,
1488
+ )
1489
+
1490
+
1491
+ @project_update_attachment.command("delete")
1492
+ @click.argument("project_uuid", metavar="PROJECT")
1493
+ @click.argument("update_uuid", metavar="UPDATE")
1494
+ @click.argument("attachment_uuid", metavar="ATTACHMENT")
1495
+ @click.option("--dry-run", is_flag=True, help="Say what would happen and send nothing.")
1496
+ @click.option("-y", "--yes", "assume_yes", is_flag=True, help="Skip the confirmation.")
1497
+ @click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
1498
+ def project_update_attachment_delete(
1499
+ project_uuid: str,
1500
+ update_uuid: str,
1501
+ attachment_uuid: str,
1502
+ dry_run: bool,
1503
+ assume_yes: bool,
1504
+ json_mode: bool,
1505
+ ) -> None:
1506
+ """Remove an attachment from a project update. Its author, or an organization admin. Cannot be undone.
1507
+
1508
+ \b
1509
+ Examples:
1510
+ dailybot project update-attachment delete <project> <update> <attachment> --dry-run
1511
+ dailybot project update-attachment delete <project> <update> <attachment> --yes
1512
+ """
1513
+ run_delete(
1514
+ lambda client: client.delete_update_attachment(project_uuid, update_uuid, attachment_uuid),
1515
+ f"delete attachment {attachment_uuid} from project update {update_uuid}.",
1516
+ receipt={"project": project_uuid, "update": update_uuid, "attachment": attachment_uuid},
1517
+ dry_run=dry_run,
1518
+ assume_yes=assume_yes,
1519
+ json_mode=json_mode,
1520
+ require_auth=require_auth,
1521
+ )
@@ -56,6 +56,11 @@ ERROR_CODE_MESSAGES: dict[str, str] = {
56
56
  # Written against what the server actually returned in the live probe
57
57
  # recorded in the plan's PERMISSION_MATRIX_OBSERVED.md, not against prose.
58
58
  #
59
+ # Project update authorship
60
+ "update_not_author": (
61
+ "Only the person who posted this project update can edit it or change its "
62
+ "attachments (an organization admin can delete it). Post a new update instead."
63
+ ),
59
64
  # Agent attribution (--agent-name / DAILYBOT_AGENT_NAME)
60
65
  "invalid_agent_attribution": (
61
66
  "The agent name was refused. Use a plain name of 128 characters or fewer: "
@@ -2062,17 +2062,54 @@ def print_milestones_table(milestones: list[dict[str, Any]], *, rollup: Any = No
2062
2062
  table.add_column("Name")
2063
2063
  table.add_column("Status", no_wrap=True)
2064
2064
  table.add_column("Open", no_wrap=True)
2065
+ table.add_column("Files", no_wrap=True)
2065
2066
  table.add_column("UUID", no_wrap=True)
2066
2067
  for row in milestones:
2068
+ files: Any = row.get("attachment_count")
2067
2069
  table.add_row(
2068
2070
  present_untrusted(row.get("name")),
2069
2071
  present_untrusted(row.get("status"), limit=16),
2070
2072
  rollup(row, "open_task_count") if rollup else "",
2073
+ str(files) if isinstance(files, int) else "",
2071
2074
  safe_text(row.get("uuid") or ""),
2072
2075
  )
2073
2076
  console.print(table)
2074
2077
 
2075
2078
 
2079
+ def print_project_updates(updates: list[dict[str, Any]]) -> None:
2080
+ """Render project updates: the person, the agent companion, health, and files.
2081
+
2082
+ The body is user-authored text and is quoted as data. The person who posted
2083
+ stays primary; `via <agent>` names the agent that executed the post.
2084
+ """
2085
+ if not updates:
2086
+ print_info("No updates.")
2087
+ return
2088
+ for update in updates:
2089
+ author: Any = update.get("created_by") or update.get("author") or {}
2090
+ who: str = present_untrusted(
2091
+ _person_name(author) if isinstance(author, dict) else author, limit=40
2092
+ )
2093
+ agent: Any = update.get("executed_by_agent")
2094
+ if isinstance(agent, dict) and agent.get("name"):
2095
+ who += f" [dim]via {present_untrusted(agent.get('name'), limit=40)}[/dim]"
2096
+ meta: list[str] = [safe_text(update.get("created_at") or "")]
2097
+ if update.get("health"):
2098
+ meta.append(safe_text(update.get("health")))
2099
+ if update.get("edited_at"):
2100
+ meta.append("edited")
2101
+ attachments: Any = update.get("attachments")
2102
+ count: Any = update.get("attachment_count")
2103
+ if isinstance(attachments, list) and attachments:
2104
+ meta.append(f"{len(attachments)} file(s)")
2105
+ elif isinstance(count, int) and count > 0:
2106
+ meta.append(f"{count} file(s)")
2107
+ console.print(f"[dim]{' · '.join(m for m in meta if m)}[/dim] {who}")
2108
+ console.print(f" {present_untrusted(update.get('body'), limit=400)}")
2109
+ if update.get("uuid"):
2110
+ console.print(f" [dim]{safe_text(update.get('uuid'))}[/dim]")
2111
+
2112
+
2076
2113
  def _threaded(comments: list[dict[str, Any]]) -> list[dict[str, Any]]:
2077
2114
  """Each root comment followed by its replies, in list order.
2078
2115
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dailybot-cli
3
- Version: 3.20.0
3
+ Version: 3.21.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
@@ -992,6 +992,8 @@ humans who own them.
992
992
  | `dailybot project get <uuid>` | Show one project |
993
993
  | `dailybot project updates [<project>]` | Batched update digest, or one project's feed |
994
994
  | `dailybot project update-post <uuid> <body>` | **Post a project update** — how the team sees what was done (`--health`; sends an idempotency key) |
995
+ | `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
+ | `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>` |
995
997
  | `dailybot project milestones [<uuid>]` | List milestones |
996
998
  | `dailybot project milestone-complete <p> <m>` | Complete a milestone. **Its open tasks stay open** |
997
999
  | `dailybot project milestone-reopen <p> <m>` | Reopen a milestone |
@@ -1001,6 +1003,7 @@ humans who own them.
1001
1003
  | `dailybot project members <uuid>` · `project member add\|remove` | Who can see the project — people or whole teams (`--team`) — `members` works with a personal API key; `member add\|remove` **needs a person: `dailybot login` or a personal API key** |
1002
1004
  | `dailybot project views <uuid>` · `project view save` | Your saved views; save replaces the list and requires the ETag — **needs a person: `dailybot login` or a personal API key** |
1003
1005
  | `dailybot project milestone-create\|milestone-update\|milestone-delete` | Dated milestones; delete retires it (tasks keep pointing at it) |
1006
+ | `dailybot project milestone-attach\|milestone-attachments <project> <milestone>` · `milestone-attachment get\|rename\|delete` | Files on a milestone (≤5 MiB); reference one in the description with `attachment:<uuid>`. `project milestone-restore` brings a retired milestone back |
1004
1007
  | `dailybot project archive <uuid>` | Archive a project |
1005
1008
  | `dailybot project attach <uuid> <file>` · `project attachments` · `project attachment get\|delete` | Files on a project (≤5 MiB, one request). Attaching and deleting need a person (`dailybot login` or a personal API key) |
1006
1009
  | `dailybot goal list` | List goals (`--include` is repeatable: `--include progress --include projects`) |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dailybot-cli"
7
- version = "3.20.0"
7
+ version = "3.21.0"
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