dailybot-cli 3.0.2__tar.gz → 3.1.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 (49) hide show
  1. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/PKG-INFO +8 -8
  2. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/README.md +7 -7
  3. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/api_client.py +70 -23
  4. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/commands/checkin.py +10 -0
  5. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/commands/form.py +37 -4
  6. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/commands/interactive.py +3 -3
  7. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/commands/kudos.py +54 -16
  8. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/commands/public_api_helpers.py +32 -2
  9. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/commands/query_options.py +44 -1
  10. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/commands/user_scoped_actions.py +60 -9
  11. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/commands/workflow.py +4 -2
  12. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/display.py +37 -21
  13. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/tui/app.py +17 -19
  14. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli.egg-info/PKG-INFO +8 -8
  15. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/pyproject.toml +1 -1
  16. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/LICENSE +0 -0
  17. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/__init__.py +0 -0
  18. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/commands/__init__.py +0 -0
  19. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/commands/agent.py +0 -0
  20. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/commands/ask.py +0 -0
  21. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/commands/auth.py +0 -0
  22. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/commands/authoring_helpers.py +0 -0
  23. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/commands/channels.py +0 -0
  24. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/commands/chat.py +0 -0
  25. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/commands/config.py +0 -0
  26. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/commands/hook.py +0 -0
  27. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/commands/identity.py +0 -0
  28. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/commands/interactive_chat.py +0 -0
  29. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/commands/status.py +0 -0
  30. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/commands/team.py +0 -0
  31. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/commands/uninstall.py +0 -0
  32. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/commands/update.py +0 -0
  33. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/commands/upgrade.py +0 -0
  34. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/commands/user.py +0 -0
  35. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/commands/version.py +0 -0
  36. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/config.py +0 -0
  37. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/install_method.py +0 -0
  38. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/ledger.py +0 -0
  39. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/main.py +0 -0
  40. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/tui/__init__.py +0 -0
  41. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/tui/commands.py +0 -0
  42. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/tui/conversation.py +0 -0
  43. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli/tui/intents.py +0 -0
  44. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli.egg-info/SOURCES.txt +0 -0
  45. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli.egg-info/dependency_links.txt +0 -0
  46. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli.egg-info/entry_points.txt +0 -0
  47. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli.egg-info/requires.txt +0 -0
  48. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/dailybot_cli.egg-info/top_level.txt +0 -0
  49. {dailybot_cli-3.0.2 → dailybot_cli-3.1.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dailybot-cli
3
- Version: 3.0.2
3
+ Version: 3.1.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
@@ -489,8 +489,8 @@ dailybot kudos list --filter KUDOS_RECEIVED
489
489
  dailybot kudos list --filter KUDOS_GIVEN --since 2026-01-01 --until 2026-06-30
490
490
  dailybot kudos list --search "release" --all --json
491
491
 
492
- # Org-wide kudos stats
493
- dailybot kudos org --json
492
+ # Every kudos in the organization (admin-only)
493
+ dailybot kudos org --page-size 20 --since 2026-07-01
494
494
 
495
495
  # Leaderboard of top kudos recipients
496
496
  dailybot kudos wall-of-fame --limit 10
@@ -499,9 +499,9 @@ dailybot kudos wall-of-fame --limit 10
499
499
  `dailybot kudos list` accepts the shared list flags (pagination, `--search`, and
500
500
  date filters) — see [Listing, search, and pagination](#listing-search-and-pagination).
501
501
 
502
- > `dailybot kudos org` is **API-key-only**: the org-stats endpoint rejects a CLI
503
- > login (Bearer) session with `403`. Set `DAILYBOT_API_KEY` (or
504
- > `dailybot config key=...`) before running it.
502
+ > `dailybot kudos org` is **admin-only** and shows the whole organization's kudos
503
+ > feed, where `dailybot kudos list` shows only the ones you gave or received. It
504
+ > accepts the same list flags.
505
505
 
506
506
  ---
507
507
 
@@ -564,7 +564,7 @@ envelope, and list output prints a `Showing X of N` count footer.
564
564
  | Flag | Description |
565
565
  |------|-------------|
566
566
  | `--page N` | Fetch a specific 1-based page. |
567
- | `--page-size N` | Results per page (max 200). |
567
+ | `--page-size N` | Results per page (max 100). |
568
568
  | `--all` | Fetch every page and merge the results. |
569
569
  | `--limit N` | Stop after N results. |
570
570
  | `--search TEXT` / `--grep TEXT` | Filter by a search term (max 256 chars, truncated client-side). |
@@ -892,7 +892,7 @@ Replies to agent emails land as messages retrievable via `dailybot agent message
892
892
  |---------|-------------|
893
893
  | `dailybot kudos give` | Give kudos to a user (`--to`), a team (`--team`), or both |
894
894
  | `dailybot kudos list` | List kudos you've received or given (`--filter`, search, date flags) |
895
- | `dailybot kudos org` | Org-wide kudos stats (**API-key-only**) |
895
+ | `dailybot kudos org` | Every kudos in the organization (**admin-only**) |
896
896
  | `dailybot kudos wall-of-fame` | Leaderboard of top kudos recipients (`--limit`) |
897
897
 
898
898
  ### Team
@@ -450,8 +450,8 @@ dailybot kudos list --filter KUDOS_RECEIVED
450
450
  dailybot kudos list --filter KUDOS_GIVEN --since 2026-01-01 --until 2026-06-30
451
451
  dailybot kudos list --search "release" --all --json
452
452
 
453
- # Org-wide kudos stats
454
- dailybot kudos org --json
453
+ # Every kudos in the organization (admin-only)
454
+ dailybot kudos org --page-size 20 --since 2026-07-01
455
455
 
456
456
  # Leaderboard of top kudos recipients
457
457
  dailybot kudos wall-of-fame --limit 10
@@ -460,9 +460,9 @@ dailybot kudos wall-of-fame --limit 10
460
460
  `dailybot kudos list` accepts the shared list flags (pagination, `--search`, and
461
461
  date filters) — see [Listing, search, and pagination](#listing-search-and-pagination).
462
462
 
463
- > `dailybot kudos org` is **API-key-only**: the org-stats endpoint rejects a CLI
464
- > login (Bearer) session with `403`. Set `DAILYBOT_API_KEY` (or
465
- > `dailybot config key=...`) before running it.
463
+ > `dailybot kudos org` is **admin-only** and shows the whole organization's kudos
464
+ > feed, where `dailybot kudos list` shows only the ones you gave or received. It
465
+ > accepts the same list flags.
466
466
 
467
467
  ---
468
468
 
@@ -525,7 +525,7 @@ envelope, and list output prints a `Showing X of N` count footer.
525
525
  | Flag | Description |
526
526
  |------|-------------|
527
527
  | `--page N` | Fetch a specific 1-based page. |
528
- | `--page-size N` | Results per page (max 200). |
528
+ | `--page-size N` | Results per page (max 100). |
529
529
  | `--all` | Fetch every page and merge the results. |
530
530
  | `--limit N` | Stop after N results. |
531
531
  | `--search TEXT` / `--grep TEXT` | Filter by a search term (max 256 chars, truncated client-side). |
@@ -853,7 +853,7 @@ Replies to agent emails land as messages retrievable via `dailybot agent message
853
853
  |---------|-------------|
854
854
  | `dailybot kudos give` | Give kudos to a user (`--to`), a team (`--team`), or both |
855
855
  | `dailybot kudos list` | List kudos you've received or given (`--filter`, search, date flags) |
856
- | `dailybot kudos org` | Org-wide kudos stats (**API-key-only**) |
856
+ | `dailybot kudos org` | Every kudos in the organization (**admin-only**) |
857
857
  | `dailybot kudos wall-of-fame` | Leaderboard of top kudos recipients (`--limit`) |
858
858
 
859
859
  ### Team
@@ -10,14 +10,41 @@ import httpx
10
10
  from dailybot_cli.config import get_api_key, get_api_url, get_token
11
11
 
12
12
  _MAX_LIST_PAGES: int = 50 # safety cap for paginated list endpoints
13
- DEFAULT_PAGE_SIZE: int = 50 # server default page size for paginated list endpoints
14
- MAX_PAGE_SIZE: int = 200 # server clamps above this; the client clamps too
13
+ DEFAULT_PAGE_SIZE: int = 25 # server default page size for paginated list endpoints
14
+ MAX_PAGE_SIZE: int = 100 # server clamps above this; the client clamps too
15
15
 
16
16
  MAX_RATE_LIMIT_RETRIES: int = 3 # attempts to retry a generic 429 before raising
17
17
  DEFAULT_RETRY_AFTER_SECS: float = 1.0 # backoff floor when a 429 omits Retry-After
18
18
  # A free-plan daily throttle is NOT transient — retrying can never succeed today.
19
19
  FREE_PLAN_DAILY_LIMIT_CODE: str = "free_plan_daily_limit_exceeded"
20
20
 
21
+ MAX_FALLBACK_DETAIL_CHARS: int = 160 # cap for a non-JSON error body echoed to the user
22
+
23
+
24
+ def _fallback_detail(response: httpx.Response) -> str:
25
+ """Build an error detail when the body is not the expected JSON envelope.
26
+
27
+ A 5xx can return a rendered HTML page. Echoing that verbatim floods the
28
+ terminal and leaks server internals (tracebacks, settings, file paths), so
29
+ only short non-HTML bodies are surfaced.
30
+ """
31
+ content_type: str = response.headers.get("content-type", "")
32
+ text: str = (response.text or "").strip()
33
+ if not text or "html" in content_type.lower() or text.startswith("<"):
34
+ return f"HTTP {response.status_code}"
35
+ return text[:MAX_FALLBACK_DETAIL_CHARS]
36
+
37
+
38
+ def resource_uuid(payload: dict[str, Any]) -> str:
39
+ """Return a resource's canonical identifier from an API payload.
40
+
41
+ The API is mid-migration from ``id`` to ``uuid``: forms and their responses
42
+ now expose only ``uuid``, agent resources expose both with the same value,
43
+ and check-ins / kudos / workflows still expose only ``id``. Reading through
44
+ this helper keeps every caller correct under all three shapes.
45
+ """
46
+ return str(payload.get("uuid") or payload.get("id") or "")
47
+
21
48
 
22
49
  @dataclass
23
50
  class PaginatedResult:
@@ -151,8 +178,11 @@ class DailyBotClient:
151
178
  if isinstance(upgrade_url, str):
152
179
  extra["upgrade_url"] = upgrade_url
153
180
  except Exception:
154
- detail = response.text or f"HTTP {response.status_code}"
155
- if response.status_code in (401, 403) and self._agent_auth_mode == "bearer":
181
+ detail = _fallback_detail(response)
182
+ # Only a 401 means the session is unusable. A 403 is an authorization
183
+ # verdict (wrong role, wrong plan) whose server detail explains the
184
+ # actual cause — overwriting it sends the user into a re-login loop.
185
+ if response.status_code == 401 and self._agent_auth_mode == "bearer":
156
186
  detail = "Session expired. Run 'dailybot login' to re-authenticate."
157
187
  retry_after: float | None = None
158
188
  if response.status_code == 429:
@@ -216,23 +246,19 @@ class DailyBotClient:
216
246
  page_size: int | None = None,
217
247
  fetch_all: bool = False,
218
248
  limit: int | None = None,
219
- force_paginated: bool = False,
220
249
  ) -> PaginatedResult:
221
250
  """GET a list endpoint, tolerating both the DRF envelope and a bare array.
222
251
 
223
252
  - ``page`` / ``page_size`` are sent when provided; ``page_size`` is clamped
224
253
  to ``[1, MAX_PAGE_SIZE]``. ``limit`` is a legacy alias that also caps the
225
254
  total number of collected items.
226
- - ``force_paginated`` adds ``paginated=true`` to the query — required for the
227
- two forms endpoints (``/v1/forms/`` and ``/v1/forms/<uuid>/responses/``)
228
- that return a bare array unless asked for the envelope.
229
255
  - ``fetch_all`` follows ``next`` (bounded by ``_MAX_LIST_PAGES``); otherwise a
230
- single page is returned. A bare-array response has no ``next`` and always
231
- terminates after one page.
256
+ single page is returned.
257
+ - Every ``/v1`` list endpoint now returns the envelope unconditionally. The
258
+ bare-array branch below is kept only so an older deployment degrades to a
259
+ single page instead of raising.
232
260
  """
233
261
  query: dict[str, Any] = dict(params) if params else {}
234
- if force_paginated:
235
- query["paginated"] = "true"
236
262
  if page is not None:
237
263
  query["page"] = page
238
264
  effective_page_size: int | None = page_size if page_size is not None else limit
@@ -780,8 +806,8 @@ class DailyBotClient:
780
806
  ) -> list[dict[str, Any]]:
781
807
  """GET /v1/forms/ — optionally expand questions, search, and page.
782
808
 
783
- Always requests the envelope (``?paginated=true``). When ``meta`` is given
784
- it is populated with ``count`` / ``next`` for a pagination footer.
809
+ When ``meta`` is given it is populated with ``count`` / ``next`` for a
810
+ pagination footer.
785
811
  """
786
812
  params: dict[str, Any] = {}
787
813
  if include_questions:
@@ -796,7 +822,6 @@ class DailyBotClient:
796
822
  page_size=page_size,
797
823
  fetch_all=fetch_all,
798
824
  limit=limit,
799
- force_paginated=True,
800
825
  )
801
826
  _fill_meta(meta, result)
802
827
  return result.results
@@ -868,7 +893,6 @@ class DailyBotClient:
868
893
  page_size=page_size,
869
894
  fetch_all=fetch_all,
870
895
  limit=limit,
871
- force_paginated=True,
872
896
  )
873
897
  _fill_meta(meta, result)
874
898
  return result.results
@@ -1210,6 +1234,8 @@ class DailyBotClient:
1210
1234
  self,
1211
1235
  *,
1212
1236
  search: str | None = None,
1237
+ start_date: str | None = None,
1238
+ end_date: str | None = None,
1213
1239
  page: int | None = None,
1214
1240
  page_size: int | None = None,
1215
1241
  fetch_all: bool = True,
@@ -1218,7 +1244,7 @@ class DailyBotClient:
1218
1244
  ) -> list[dict[str, Any]]:
1219
1245
  """GET /v1/workflows/ — list workflows (read-only; plan-gated feature)."""
1220
1246
  params: dict[str, Any] = {}
1221
- _merge_list_query(params, search=search)
1247
+ _merge_list_query(params, search=search, start_date=start_date, end_date=end_date)
1222
1248
  result: PaginatedResult = self._paginated_get(
1223
1249
  f"{self.api_url}/v1/workflows/",
1224
1250
  params=params,
@@ -1239,14 +1265,35 @@ class DailyBotClient:
1239
1265
  )
1240
1266
  return self._handle_response(response)
1241
1267
 
1242
- def get_kudos_organization(self) -> dict[str, Any]:
1243
- """GET /v1/kudos/organization/ — org-wide kudos statistics."""
1244
- response: httpx.Response = httpx.get(
1268
+ def list_kudos_organization(
1269
+ self,
1270
+ *,
1271
+ search: str | None = None,
1272
+ start_date: str | None = None,
1273
+ end_date: str | None = None,
1274
+ page: int | None = None,
1275
+ page_size: int | None = None,
1276
+ fetch_all: bool = True,
1277
+ limit: int | None = None,
1278
+ meta: dict[str, Any] | None = None,
1279
+ ) -> list[dict[str, Any]]:
1280
+ """GET /v1/kudos/organization/ — every kudos in the org (admin-only).
1281
+
1282
+ The org-wide counterpart of ``list_kudos``, which is scoped to the caller.
1283
+ Returns the same paginated envelope, not an aggregate statistics object.
1284
+ """
1285
+ params: dict[str, Any] = {}
1286
+ _merge_list_query(params, search=search, start_date=start_date, end_date=end_date)
1287
+ result: PaginatedResult = self._paginated_get(
1245
1288
  f"{self.api_url}/v1/kudos/organization/",
1246
- headers=self._headers(),
1247
- timeout=self.timeout,
1289
+ params=params,
1290
+ page=page,
1291
+ page_size=page_size,
1292
+ fetch_all=fetch_all,
1293
+ limit=limit,
1248
1294
  )
1249
- return self._handle_response(response)
1295
+ _fill_meta(meta, result)
1296
+ return result.results
1250
1297
 
1251
1298
  def get_kudos_wall_of_fame(self, *, limit: int | None = None) -> dict[str, Any]:
1252
1299
  """GET /v1/kudos/wall-of-fame/ — leaderboard rankings."""
@@ -31,7 +31,9 @@ from dailybot_cli.commands.public_api_helpers import (
31
31
  enforce_plan_access,
32
32
  exit_for_api_error,
33
33
  require_auth,
34
+ validate_user_filter,
34
35
  )
36
+ from dailybot_cli.commands.query_options import paging_options
35
37
  from dailybot_cli.commands.user_scoped_actions import (
36
38
  execute_checkin_complete,
37
39
  execute_checkin_edit,
@@ -307,6 +309,7 @@ def checkin_show(followup_uuid: str, json_mode: bool) -> None:
307
309
  @click.option(
308
310
  "--search", "-s", "search", default=None, help="Filter response content (max 256 chars)."
309
311
  )
312
+ @paging_options
310
313
  @click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
311
314
  def checkin_history(
312
315
  followup_uuid: str,
@@ -315,6 +318,9 @@ def checkin_history(
315
318
  date_to: str | None,
316
319
  user: str | None,
317
320
  search: str | None,
321
+ page: int | None,
322
+ page_size: int | None,
323
+ limit: int | None,
318
324
  json_mode: bool,
319
325
  ) -> None:
320
326
  """Show a check-in's response history.
@@ -330,6 +336,7 @@ def checkin_history(
330
336
  dailybot checkin history <followup_uuid> --from 2026-06-01 --to 2026-06-30 --json
331
337
  dailybot checkin history <followup_uuid> --user <user_uuid> --days 30
332
338
  """
339
+ validate_user_filter(user)
333
340
  client = require_auth()
334
341
  execute_checkin_history(
335
342
  client,
@@ -339,6 +346,9 @@ def checkin_history(
339
346
  date_to=date_to,
340
347
  user=user,
341
348
  search=search,
349
+ page=page,
350
+ page_size=page_size,
351
+ limit=limit,
342
352
  json_mode=json_mode,
343
353
  )
344
354
 
@@ -26,8 +26,13 @@ from dailybot_cli.commands.public_api_helpers import (
26
26
  enforce_plan_access,
27
27
  exit_for_api_error,
28
28
  require_auth,
29
+ validate_user_filter,
30
+ )
31
+ from dailybot_cli.commands.query_options import (
32
+ build_query_params,
33
+ paging_options,
34
+ query_options,
29
35
  )
30
- from dailybot_cli.commands.query_options import build_query_params, query_options
31
36
  from dailybot_cli.commands.user_scoped_actions import (
32
37
  execute_form_list,
33
38
  execute_form_submit,
@@ -51,6 +56,16 @@ from dailybot_cli.display import (
51
56
  print_success,
52
57
  )
53
58
 
59
+ # The server reuses `invalid_workflow_state` for a malformed `--state
60
+ # "Label:#color"` during authoring. On this listing it means the form has no
61
+ # workflow at all, so the shared message would send the user down the wrong path.
62
+ _RESPONSES_ERROR_OVERRIDES: dict[str, str] = {
63
+ "invalid_workflow_state": (
64
+ "This form has no workflow, so --state doesn't apply. Drop the flag, or run "
65
+ "`dailybot form get <form_uuid>` to see the states a workflow form defines."
66
+ ),
67
+ }
68
+
54
69
 
55
70
  def _maybe_load_form(client: DailyBotClient, form_uuid: str) -> dict[str, Any] | None:
56
71
  """Return form metadata if accessible; swallow errors (UI-only enrichment)."""
@@ -293,7 +308,11 @@ def form_submit(
293
308
 
294
309
  @form.command("responses")
295
310
  @click.argument("form_uuid")
296
- @click.option("--state", default=None, help="Filter by current_state (workflow forms only).")
311
+ @click.option(
312
+ "--state",
313
+ default=None,
314
+ help="Filter by workflow state key (e.g. draft, not Draft). Workflow forms only.",
315
+ )
297
316
  @click.option(
298
317
  "--all",
299
318
  "all_responses",
@@ -313,6 +332,7 @@ def form_submit(
313
332
  is_flag=True,
314
333
  help="Return only the most recent response (continue where you left off).",
315
334
  )
335
+ @paging_options
316
336
  @click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
317
337
  def form_responses(
318
338
  form_uuid: str,
@@ -323,6 +343,9 @@ def form_responses(
323
343
  date_to: str | None,
324
344
  search: str | None,
325
345
  latest: bool,
346
+ page: int | None,
347
+ page_size: int | None,
348
+ limit: int | None,
326
349
  json_mode: bool,
327
350
  ) -> None:
328
351
  """List responses on a form.
@@ -339,6 +362,7 @@ def form_responses(
339
362
  dailybot form responses <form_uuid> --all --from 2026-01-01 --to 2026-06-30
340
363
  dailybot form responses <form_uuid> --user <user_uuid> --json
341
364
  """
365
+ validate_user_filter(user)
342
366
  client = require_auth()
343
367
  truncated_search: str | None = search[:256] if search is not None else None
344
368
  meta: dict[str, Any] = {}
@@ -352,10 +376,14 @@ def form_responses(
352
376
  date_from=date_from,
353
377
  date_to=date_to,
354
378
  search=truncated_search,
379
+ page=page,
380
+ page_size=page_size,
381
+ fetch_all=page is None and page_size is None and limit is None,
382
+ limit=limit,
355
383
  meta=meta,
356
384
  )
357
385
  except APIError as exc:
358
- exit_for_api_error(exc, json_mode)
386
+ exit_for_api_error(exc, json_mode, code_overrides=_RESPONSES_ERROR_OVERRIDES)
359
387
 
360
388
  if latest:
361
389
  responses = responses[:1] if responses else []
@@ -366,7 +394,12 @@ def form_responses(
366
394
 
367
395
  form_data: dict[str, Any] | None = _maybe_load_form(client, form_uuid)
368
396
  print_form_responses_table(form_uuid, responses, form_data)
369
- print_pagination_footer(len(responses), meta.get("count"), has_more=bool(meta.get("next")))
397
+ print_pagination_footer(
398
+ len(responses),
399
+ meta.get("count"),
400
+ has_more=bool(meta.get("next")),
401
+ more_hint="omit --page/--page-size to fetch every page",
402
+ )
370
403
 
371
404
 
372
405
  @form.group("response")
@@ -10,7 +10,7 @@ import questionary
10
10
  from questionary import Choice, Separator
11
11
 
12
12
  from dailybot_cli import __version__
13
- from dailybot_cli.api_client import APIError, DailyBotClient
13
+ from dailybot_cli.api_client import APIError, DailyBotClient, resource_uuid
14
14
  from dailybot_cli.commands.auth import _do_login
15
15
  from dailybot_cli.commands.chat import ChatPayloadError, build_chat_payload
16
16
  from dailybot_cli.commands.kudos import execute_kudos_give
@@ -94,7 +94,7 @@ def _checkin_label(checkin: dict[str, Any]) -> str:
94
94
 
95
95
 
96
96
  def _form_label(form: dict[str, Any]) -> str:
97
- name: str = str(form.get("name") or form.get("id") or "Form")
97
+ name: str = str(form.get("name") or resource_uuid(form) or "Form")
98
98
  active: bool = bool(form.get("is_active"))
99
99
  suffix: str = "" if active else " [inactive]"
100
100
  return f"{name}{suffix}"
@@ -269,7 +269,7 @@ def _submit_form(client: DailyBotClient) -> None:
269
269
  if selected is None:
270
270
  raise InteractiveAbort()
271
271
 
272
- form_uuid: str = str(selected.get("id") or "")
272
+ form_uuid: str = resource_uuid(selected)
273
273
  form_name: str = str(selected.get("name") or form_uuid)
274
274
  if not form_uuid:
275
275
  print_error("Selected form has no UUID.")
@@ -17,7 +17,7 @@ from dailybot_cli.commands.public_api_helpers import (
17
17
  resolve_team_by_name_or_uuid,
18
18
  resolve_user_by_name_or_uuid,
19
19
  )
20
- from dailybot_cli.commands.query_options import build_query_params, query_options
20
+ from dailybot_cli.commands.query_options import build_query_params, query_options, resolve_fetch_all
21
21
  from dailybot_cli.display import (
22
22
  console,
23
23
  print_detail_panel,
@@ -211,13 +211,6 @@ def kudos_give(
211
211
  )
212
212
 
213
213
 
214
- _KUDOS_ORG_FIELDS: list[tuple[str, str]] = [
215
- ("Total kudos", "total_kudos"),
216
- ("Total givers", "total_givers"),
217
- ("Total receivers", "total_receivers"),
218
- ]
219
-
220
-
221
214
  @kudos.command("list")
222
215
  @click.option(
223
216
  "--filter", "kudos_filter", default=None, help="Filter (e.g. KUDOS_RECEIVED, KUDOS_GIVEN)."
@@ -264,7 +257,7 @@ def kudos_list(
264
257
  print_error(str(exc))
265
258
  raise SystemExit(EXIT_USAGE_ERROR)
266
259
  client = require_auth()
267
- resolved_fetch_all: bool = spec.fetch_all or (spec.page is None and spec.limit is None)
260
+ resolved_fetch_all: bool = resolve_fetch_all(spec)
268
261
  meta: dict[str, Any] = {}
269
262
  try:
270
263
  with console.status("Fetching kudos..."):
@@ -289,26 +282,71 @@ def kudos_list(
289
282
 
290
283
 
291
284
  @kudos.command("org")
285
+ @query_options
292
286
  @click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
293
- def kudos_org(json_mode: bool) -> None:
294
- """Show org-wide kudos statistics.
287
+ def kudos_org(
288
+ json_mode: bool,
289
+ page: int | None,
290
+ page_size: int | None,
291
+ fetch_all: bool,
292
+ limit: int | None,
293
+ search: str | None,
294
+ since: str | None,
295
+ until: str | None,
296
+ on_date: str | None,
297
+ last_week: bool,
298
+ today: bool,
299
+ ) -> None:
300
+ """Browse every kudos in the organization (admin only).
301
+
302
+ \b
303
+ The org-wide feed. `kudos list` shows only the ones you gave or received.
304
+ Non-admins receive a 403.
295
305
 
296
306
  \b
297
307
  Examples:
298
308
  dailybot kudos org
299
- dailybot kudos org --json
309
+ dailybot kudos org --page-size 20 --since 2026-07-01
310
+ dailybot kudos org --search onboarding --json
300
311
  """
301
312
  enforce_plan_access("kudos_org", json_mode=json_mode)
313
+ try:
314
+ spec = build_query_params(
315
+ page=page,
316
+ page_size=page_size,
317
+ fetch_all=fetch_all,
318
+ limit=limit,
319
+ search=search,
320
+ since=since,
321
+ until=until,
322
+ on_date=on_date,
323
+ last_week=last_week,
324
+ today=today,
325
+ )
326
+ except ValueError as exc:
327
+ print_error(str(exc))
328
+ raise SystemExit(EXIT_USAGE_ERROR)
302
329
  client = require_auth()
330
+ meta: dict[str, Any] = {}
303
331
  try:
304
- with console.status("Fetching kudos stats..."):
305
- data: dict[str, Any] = client.get_kudos_organization()
332
+ with console.status("Fetching organization kudos..."):
333
+ kudos_items: list[dict[str, Any]] = client.list_kudos_organization(
334
+ search=spec.params.get("search"),
335
+ start_date=spec.params.get("start_date"),
336
+ end_date=spec.params.get("end_date"),
337
+ page=spec.page,
338
+ page_size=spec.page_size,
339
+ fetch_all=resolve_fetch_all(spec),
340
+ limit=spec.limit,
341
+ meta=meta,
342
+ )
306
343
  except APIError as exc:
307
344
  exit_for_api_error(exc, json_mode)
308
345
  if json_mode:
309
- emit_json(data)
346
+ emit_json(kudos_items)
310
347
  return
311
- print_detail_panel("Kudos — Organization", data, _KUDOS_ORG_FIELDS)
348
+ print_kudos_table(kudos_items)
349
+ print_pagination_footer(len(kudos_items), meta.get("count"), has_more=bool(meta.get("next")))
312
350
 
313
351
 
314
352
  @kudos.command("wall-of-fame")
@@ -211,7 +211,8 @@ ERROR_CODE_MESSAGES: dict[str, str] = {
211
211
  # `extra` (upgrade_url, required/current role) in exit_for_api_error.
212
212
  "plan_upgrade_required": (
213
213
  "This action requires a paid Dailybot plan. On the free plan the CLI can "
214
- "only submit agent reports, send agent emails, and read your own profile."
214
+ "only submit agent reports and emails, check agent messages and health, "
215
+ "read your own profile, and view today's pending check-ins."
215
216
  ),
216
217
  "plan_free_api_keys_forbidden": (
217
218
  "API keys aren't available on the free plan. Run `dailybot login` to use a "
@@ -236,6 +237,10 @@ ERROR_CODE_MESSAGES: dict[str, str] = {
236
237
  "invalid_date_range": (
237
238
  "Invalid date. Use YYYY-MM-DD, and make sure the start date is on or before the end date."
238
239
  ),
240
+ "invalid_user_identifier": (
241
+ "--user only accepts a user UUID, not an email or a name. "
242
+ "Run 'dailybot user list' to find it."
243
+ ),
239
244
  # Rate limiting (429)
240
245
  "free_plan_daily_limit_exceeded": (
241
246
  "You've reached today's free-plan limit for this action. Wait for it to reset "
@@ -263,6 +268,19 @@ UUID_PATTERN: re.Pattern[str] = re.compile(
263
268
  )
264
269
 
265
270
 
271
+ def validate_user_filter(value: str | None) -> None:
272
+ """Reject a non-UUID ``--user`` filter before it reaches the API.
273
+
274
+ The response-listing endpoints accept only a UUID here. Checking locally
275
+ turns a round-trip and a 400 into an immediate, actionable message.
276
+ """
277
+ if value is None:
278
+ return
279
+ if not UUID_PATTERN.match(value):
280
+ print_error(ERROR_CODE_MESSAGES["invalid_user_identifier"])
281
+ raise SystemExit(1)
282
+
283
+
266
284
  def require_auth() -> DailyBotClient:
267
285
  """Ensure a login session or API key exists and return a client.
268
286
 
@@ -305,18 +323,30 @@ def _augment_code_message(base: str, code: str, extra: dict[str, Any]) -> str:
305
323
  return base
306
324
 
307
325
 
308
- def exit_for_api_error(exc: APIError, json_mode: bool) -> NoReturn:
326
+ def exit_for_api_error(
327
+ exc: APIError,
328
+ json_mode: bool,
329
+ *,
330
+ code_overrides: dict[str, str] | None = None,
331
+ ) -> NoReturn:
309
332
  """Map API failures to user-facing messages and process exit codes.
310
333
 
311
334
  Dispatches strictly on the machine-readable ``code`` (never on ``detail``
312
335
  text); falls back to the status-code-based message when ``code`` is absent
313
336
  (older backends).
337
+
338
+ ``code_overrides`` lets a command replace the shared message for a code the
339
+ server reuses across surfaces. ``invalid_workflow_state`` means "malformed
340
+ --state definition" when authoring a form and "this form has no workflow"
341
+ when filtering its responses; only the calling command can tell them apart.
314
342
  """
315
343
  code: str | None = getattr(exc, "code", None)
316
344
  extra: dict[str, Any] = getattr(exc, "extra", {}) or {}
317
345
  mapped_message: str | None = ERROR_CODE_MESSAGES.get(code) if code else None
318
346
  if mapped_message and code:
319
347
  mapped_message = _augment_code_message(mapped_message, code, extra)
348
+ if code and code_overrides and code in code_overrides:
349
+ mapped_message = code_overrides[code]
320
350
 
321
351
  if exc.status_code == 401:
322
352
  message: str = "Session expired. Run: dailybot login"