dailybot-cli 3.4.0__tar.gz → 3.5.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/PKG-INFO +1 -1
  2. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/api_client.py +123 -40
  3. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/form.py +192 -7
  4. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/public_api_helpers.py +12 -0
  5. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/user_scoped_actions.py +29 -2
  6. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli.egg-info/PKG-INFO +1 -1
  7. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/pyproject.toml +1 -1
  8. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/LICENSE +0 -0
  9. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/README.md +0 -0
  10. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/__init__.py +0 -0
  11. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/__init__.py +0 -0
  12. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/agent.py +0 -0
  13. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/ask.py +0 -0
  14. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/auth.py +0 -0
  15. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/authoring_helpers.py +0 -0
  16. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/channels.py +0 -0
  17. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/chat.py +0 -0
  18. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/checkin.py +0 -0
  19. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/config.py +0 -0
  20. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/conversation.py +0 -0
  21. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/hook.py +0 -0
  22. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/identity.py +0 -0
  23. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/interactive.py +0 -0
  24. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/interactive_chat.py +0 -0
  25. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/kudos.py +0 -0
  26. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/query_options.py +0 -0
  27. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/status.py +0 -0
  28. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/team.py +0 -0
  29. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/uninstall.py +0 -0
  30. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/update.py +0 -0
  31. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/upgrade.py +0 -0
  32. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/user.py +0 -0
  33. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/version.py +0 -0
  34. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/commands/workflow.py +0 -0
  35. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/config.py +0 -0
  36. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/display.py +0 -0
  37. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/install_method.py +0 -0
  38. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/ledger.py +0 -0
  39. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/main.py +0 -0
  40. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/tui/__init__.py +0 -0
  41. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/tui/app.py +0 -0
  42. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/tui/commands.py +0 -0
  43. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/tui/conversation.py +0 -0
  44. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli/tui/intents.py +0 -0
  45. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli.egg-info/SOURCES.txt +0 -0
  46. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli.egg-info/dependency_links.txt +0 -0
  47. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli.egg-info/entry_points.txt +0 -0
  48. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli.egg-info/requires.txt +0 -0
  49. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/dailybot_cli.egg-info/top_level.txt +0 -0
  50. {dailybot_cli-3.4.0 → dailybot_cli-3.5.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dailybot-cli
3
- Version: 3.4.0
3
+ Version: 3.5.1
4
4
  Summary: Dailybot CLI - The command-line bridge between humans and agents. Progress reports, observability, and workflow automation for modern teams across Slack, Google Chat, Discord, MS Teams, and more.
5
5
  Author-email: Dailybot <support@dailybot.com>
6
6
  License: MIT
@@ -156,21 +156,77 @@ class DailyBotClient:
156
156
  return headers
157
157
 
158
158
  def _agent_headers(self) -> dict[str, str]:
159
- """Build headers for agent authentication (API key preferred, then Bearer)."""
159
+ """Build headers for agent authentication.
160
+
161
+ Uses the same priority as ``_headers()`` — Bearer first, API key
162
+ second — so that all endpoints behave consistently. The server
163
+ accepts both on every ``/v1/agent*`` endpoint.
164
+ """
160
165
  headers: dict[str, str] = {
161
166
  "Content-Type": "application/json",
162
167
  "Accept": "application/json",
163
168
  }
164
- if self.api_key:
165
- headers["X-API-KEY"] = self.api_key
166
- self._agent_auth_mode = "api_key"
167
- elif self.token:
169
+ if self.token:
168
170
  headers["Authorization"] = f"Bearer {self.token}"
169
171
  self._agent_auth_mode = "bearer"
172
+ elif self.api_key:
173
+ headers["X-API-KEY"] = self.api_key
174
+ self._agent_auth_mode = "api_key"
170
175
  else:
171
176
  self._agent_auth_mode = None
172
177
  return headers
173
178
 
179
+ def _alt_auth_headers(self) -> dict[str, str] | None:
180
+ """Build headers using the alternative credential for a 401 retry.
181
+
182
+ If the primary was Bearer, tries API key; if the primary was API key,
183
+ tries Bearer. Returns ``None`` when no alternative is available.
184
+ """
185
+ headers: dict[str, str] = {
186
+ "Content-Type": "application/json",
187
+ "Accept": "application/json",
188
+ }
189
+ if self._agent_auth_mode == "bearer" and self.api_key:
190
+ headers["X-API-KEY"] = self.api_key
191
+ self._agent_auth_mode = "api_key"
192
+ return headers
193
+ if self._agent_auth_mode == "api_key" and self.token:
194
+ headers["Authorization"] = f"Bearer {self.token}"
195
+ self._agent_auth_mode = "bearer"
196
+ return headers
197
+ return None
198
+
199
+ def _agent_request(
200
+ self,
201
+ method: str,
202
+ url: str,
203
+ *,
204
+ json: dict[str, Any] | None = None,
205
+ params: dict[str, Any] | None = None,
206
+ ) -> httpx.Response:
207
+ """Execute an agent-authenticated request with automatic alt-credential retry.
208
+
209
+ Tries with ``_agent_headers()`` (Bearer preferred, API key fallback).
210
+ If the server returns 401 and an alternative credential is available,
211
+ retries once with it. This covers both directions: expired Bearer
212
+ retried with API key, and stale API key retried with Bearer.
213
+ """
214
+ kwargs: dict[str, Any] = {"headers": self._agent_headers(), "timeout": self.timeout}
215
+ if json is not None:
216
+ kwargs["json"] = json
217
+ if params is not None:
218
+ kwargs["params"] = params
219
+
220
+ response: httpx.Response = httpx.request(method, url, **kwargs)
221
+
222
+ if response.status_code == 401:
223
+ alt: dict[str, str] | None = self._alt_auth_headers()
224
+ if alt is not None:
225
+ kwargs["headers"] = alt
226
+ response = httpx.request(method, url, **kwargs)
227
+
228
+ return response
229
+
174
230
  def _handle_response(self, response: httpx.Response) -> dict[str, Any]:
175
231
  """Parse API response and raise on errors."""
176
232
  if response.status_code >= 400:
@@ -809,6 +865,9 @@ class DailyBotClient:
809
865
  include_questions: bool = False,
810
866
  include_archived: bool = False,
811
867
  owner: str | None = None,
868
+ filter_scope: str | None = None,
869
+ order: str | None = None,
870
+ is_ascend: bool = False,
812
871
  search: str | None = None,
813
872
  start_date: str | None = None,
814
873
  end_date: str | None = None,
@@ -821,7 +880,8 @@ class DailyBotClient:
821
880
  """GET /v1/forms/ — optionally expand questions, search, and page.
822
881
 
823
882
  The default response is org-scoped (every form the caller can see on the
824
- webapp list view). Pass ``owner="me"`` to narrow to the caller's own forms.
883
+ webapp list view). Pass ``owner="me"`` to narrow to the caller's own forms,
884
+ or ``filter_scope`` to apply a server-side scope filter.
825
885
 
826
886
  When ``meta`` is given it is populated with ``count`` / ``next`` for a
827
887
  pagination footer.
@@ -833,6 +893,12 @@ class DailyBotClient:
833
893
  params["include_archived"] = "true"
834
894
  if owner:
835
895
  params["owner"] = owner
896
+ if filter_scope:
897
+ params["filter"] = filter_scope
898
+ if order:
899
+ params["order"] = order
900
+ if is_ascend:
901
+ params["is_ascend"] = "true"
836
902
  _merge_list_query(params, search=search, start_date=start_date, end_date=end_date)
837
903
  result: PaginatedResult = self._paginated_get(
838
904
  f"{self.api_url}/v1/forms/",
@@ -858,11 +924,25 @@ class DailyBotClient:
858
924
  self,
859
925
  form_uuid: str,
860
926
  content: dict[str, Any],
927
+ *,
928
+ automation: bool = False,
929
+ anonymous: bool = False,
930
+ guest_user: dict[str, str] | None = None,
931
+ submission_source: str | None = None,
861
932
  ) -> dict[str, Any]:
862
933
  """POST /v1/forms/<form_uuid>/responses/"""
934
+ payload: dict[str, Any] = {"content": content}
935
+ if automation:
936
+ payload["automation"] = True
937
+ if anonymous:
938
+ payload["anonymous"] = True
939
+ if guest_user:
940
+ payload["guest_user"] = guest_user
941
+ if submission_source:
942
+ payload["submission_source"] = submission_source
863
943
  response: httpx.Response = httpx.post(
864
944
  f"{self.api_url}/v1/forms/{form_uuid}/responses/",
865
- json={"content": content},
945
+ json=payload,
866
946
  headers=self._headers(),
867
947
  timeout=self.timeout,
868
948
  )
@@ -875,6 +955,11 @@ class DailyBotClient:
875
955
  state: str | None = None,
876
956
  all_responses: bool = False,
877
957
  user: str | None = None,
958
+ submission_sources: str | None = None,
959
+ submitter_user_ids: str | None = None,
960
+ flow_status: str | None = None,
961
+ order: str | None = None,
962
+ is_ascend: bool = False,
878
963
  date_from: str | None = None,
879
964
  date_to: str | None = None,
880
965
  search: str | None = None,
@@ -900,6 +985,16 @@ class DailyBotClient:
900
985
  params["all"] = "true"
901
986
  if user:
902
987
  params["user"] = user
988
+ if submission_sources:
989
+ params["submission_sources"] = submission_sources
990
+ if submitter_user_ids:
991
+ params["submitter_user_ids"] = submitter_user_ids
992
+ if flow_status:
993
+ params["flow_status"] = flow_status
994
+ if order:
995
+ params["order"] = order
996
+ if is_ascend:
997
+ params["is_ascend"] = "true"
903
998
  if date_from:
904
999
  params["date_from"] = date_from
905
1000
  if date_to:
@@ -1381,11 +1476,10 @@ class DailyBotClient:
1381
1476
  payload["is_milestone"] = True
1382
1477
  if co_authors:
1383
1478
  payload["co_authors"] = co_authors
1384
- response: httpx.Response = httpx.post(
1479
+ response: httpx.Response = self._agent_request(
1480
+ "POST",
1385
1481
  f"{self.api_url}/v1/agent-reports/",
1386
1482
  json=payload,
1387
- headers=self._agent_headers(),
1388
- timeout=self.timeout,
1389
1483
  )
1390
1484
  return self._handle_response(response)
1391
1485
 
@@ -1402,21 +1496,19 @@ class DailyBotClient:
1402
1496
  }
1403
1497
  if message:
1404
1498
  payload["message"] = message
1405
- response: httpx.Response = httpx.post(
1499
+ response: httpx.Response = self._agent_request(
1500
+ "POST",
1406
1501
  f"{self.api_url}/v1/agent-health/",
1407
1502
  json=payload,
1408
- headers=self._agent_headers(),
1409
- timeout=self.timeout,
1410
1503
  )
1411
1504
  return self._handle_response(response)
1412
1505
 
1413
1506
  def get_agent_health(self, agent_name: str) -> dict[str, Any]:
1414
1507
  """GET /v1/agent-health/?agent_name=..."""
1415
- response: httpx.Response = httpx.get(
1508
+ response: httpx.Response = self._agent_request(
1509
+ "GET",
1416
1510
  f"{self.api_url}/v1/agent-health/",
1417
1511
  params={"agent_name": agent_name},
1418
- headers=self._agent_headers(),
1419
- timeout=self.timeout,
1420
1512
  )
1421
1513
  return self._handle_response(response)
1422
1514
 
@@ -1435,22 +1527,19 @@ class DailyBotClient:
1435
1527
  }
1436
1528
  if webhook_secret:
1437
1529
  payload["webhook_secret"] = webhook_secret
1438
- response: httpx.Response = httpx.post(
1530
+ response: httpx.Response = self._agent_request(
1531
+ "POST",
1439
1532
  f"{self.api_url}/v1/agent-webhook/",
1440
1533
  json=payload,
1441
- headers=self._agent_headers(),
1442
- timeout=self.timeout,
1443
1534
  )
1444
1535
  return self._handle_response(response)
1445
1536
 
1446
1537
  def unregister_agent_webhook(self, agent_name: str) -> dict[str, Any]:
1447
1538
  """DELETE /v1/agent-webhook/"""
1448
- response: httpx.Response = httpx.request(
1539
+ response: httpx.Response = self._agent_request(
1449
1540
  "DELETE",
1450
1541
  f"{self.api_url}/v1/agent-webhook/",
1451
1542
  json={"agent_name": agent_name},
1452
- headers=self._agent_headers(),
1453
- timeout=self.timeout,
1454
1543
  )
1455
1544
  return self._handle_response(response)
1456
1545
 
@@ -1473,11 +1562,10 @@ class DailyBotClient:
1473
1562
  }
1474
1563
  if metadata:
1475
1564
  payload["metadata"] = metadata
1476
- response: httpx.Response = httpx.post(
1565
+ response: httpx.Response = self._agent_request(
1566
+ "POST",
1477
1567
  f"{self.api_url}/v1/agent-email/send/",
1478
1568
  json=payload,
1479
- headers=self._agent_headers(),
1480
- timeout=self.timeout,
1481
1569
  )
1482
1570
  return self._handle_response(response)
1483
1571
 
@@ -1504,11 +1592,10 @@ class DailyBotClient:
1504
1592
  parent and — when ``thread_responses`` was sent — one id per reply, all
1505
1593
  of which can be fed back in a later call to edit the same message.
1506
1594
  """
1507
- response: httpx.Response = httpx.post(
1595
+ response: httpx.Response = self._agent_request(
1596
+ "POST",
1508
1597
  f"{self.api_url}/v1/send-message/",
1509
1598
  json=payload,
1510
- headers=self._agent_headers(),
1511
- timeout=self.timeout,
1512
1599
  )
1513
1600
  return self._handle_response(response)
1514
1601
 
@@ -1524,11 +1611,10 @@ class DailyBotClient:
1524
1611
  the shared agent header logic (``X-API-KEY`` preferred, else the login
1525
1612
  Bearer token). Returns ``{"channel": "<slack-conversation-id>"}``.
1526
1613
  """
1527
- response: httpx.Response = httpx.post(
1614
+ response: httpx.Response = self._agent_request(
1615
+ "POST",
1528
1616
  f"{self.api_url}/v1/open-conversation/",
1529
1617
  json={"users_uuids": users_uuids},
1530
- headers=self._agent_headers(),
1531
- timeout=self.timeout,
1532
1618
  )
1533
1619
  return self._handle_response(response)
1534
1620
 
@@ -1559,11 +1645,10 @@ class DailyBotClient:
1559
1645
  payload["sender_type"] = sender_type
1560
1646
  if sender_name:
1561
1647
  payload["sender_name"] = sender_name
1562
- response: httpx.Response = httpx.post(
1648
+ response: httpx.Response = self._agent_request(
1649
+ "POST",
1563
1650
  f"{self.api_url}/v1/agent-messages/",
1564
1651
  json=payload,
1565
- headers=self._agent_headers(),
1566
- timeout=self.timeout,
1567
1652
  )
1568
1653
  return self._handle_response(response)
1569
1654
 
@@ -1576,11 +1661,10 @@ class DailyBotClient:
1576
1661
  params: dict[str, str] = {"agent_name": agent_name}
1577
1662
  if delivered is not None:
1578
1663
  params["delivered"] = "true" if delivered else "false"
1579
- response: httpx.Response = httpx.get(
1664
+ response: httpx.Response = self._agent_request(
1665
+ "GET",
1580
1666
  f"{self.api_url}/v1/agent-messages/",
1581
1667
  params=params,
1582
- headers=self._agent_headers(),
1583
- timeout=self.timeout,
1584
1668
  )
1585
1669
  if response.status_code >= 400:
1586
1670
  self._handle_response(response)
@@ -1596,11 +1680,10 @@ class DailyBotClient:
1596
1680
  message_ids: list[str],
1597
1681
  ) -> dict[str, Any]:
1598
1682
  """PATCH /v1/agent-messages/read/"""
1599
- response: httpx.Response = httpx.patch(
1683
+ response: httpx.Response = self._agent_request(
1684
+ "PATCH",
1600
1685
  f"{self.api_url}/v1/agent-messages/read/",
1601
1686
  json={"message_ids": message_ids},
1602
- headers=self._agent_headers(),
1603
- timeout=self.timeout,
1604
1687
  )
1605
1688
  return self._handle_response(response)
1606
1689
 
@@ -1,5 +1,6 @@
1
1
  """Form commands for the user-scoped public API."""
2
2
 
3
+ import re
3
4
  from collections.abc import Callable
4
5
  from typing import Any
5
6
 
@@ -59,6 +60,16 @@ from dailybot_cli.display import (
59
60
  # The server reuses `invalid_workflow_state` for a malformed `--state
60
61
  # "Label:#color"` during authoring. On this listing it means the form has no
61
62
  # workflow at all, so the shared message would send the user down the wrong path.
63
+ MAX_SUBMISSION_SOURCE_LENGTH: int = 512
64
+ _EMAIL_RE: re.Pattern[str] = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
65
+
66
+ FORM_LIST_FILTERS: tuple[str, ...] = ("all", "me", "public", "approval", "workflow", "archived")
67
+ FORM_LIST_ORDERS: tuple[str, ...] = ("alphabetical", "recent", "total")
68
+ RESPONSE_ORDERS: tuple[str, ...] = ("recent", "oldest")
69
+ RESPONSE_SOURCES: tuple[str, ...] = ("member", "anonymous", "automation", "public")
70
+ RESPONSE_FLOW_STATUSES: tuple[str, ...] = ("pending", "approved", "denied")
71
+ MAX_SUBMITTER_IDS: int = 50
72
+
62
73
  _RESPONSES_ERROR_OVERRIDES: dict[str, str] = {
63
74
  "invalid_workflow_state": (
64
75
  "This form has no workflow, so --state doesn't apply. Drop the flag, or run "
@@ -193,11 +204,42 @@ def form() -> None:
193
204
  is_flag=True,
194
205
  help="Only forms you own (default lists every form you can see in the org).",
195
206
  )
207
+ @click.option(
208
+ "--filter",
209
+ "filter_scope",
210
+ type=click.Choice(FORM_LIST_FILTERS, case_sensitive=False),
211
+ default=None,
212
+ help="Scope filter: all, me, public, approval, workflow, archived.",
213
+ )
214
+ @click.option(
215
+ "--order",
216
+ type=click.Choice(FORM_LIST_ORDERS, case_sensitive=False),
217
+ default=None,
218
+ help="Sort order: alphabetical, recent, or total responses.",
219
+ )
220
+ @click.option(
221
+ "--ascending",
222
+ "--asc",
223
+ "is_ascend",
224
+ is_flag=True,
225
+ default=False,
226
+ help="Sort ascending (default: descending).",
227
+ )
228
+ @click.option(
229
+ "--include-questions",
230
+ is_flag=True,
231
+ default=False,
232
+ help="Include question definitions in each form.",
233
+ )
196
234
  @query_options
197
235
  @click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
198
236
  def form_list(
199
237
  include_archived: bool,
200
238
  mine: bool,
239
+ filter_scope: str | None,
240
+ order: str | None,
241
+ is_ascend: bool,
242
+ include_questions: bool,
201
243
  json_mode: bool,
202
244
  page: int | None,
203
245
  page_size: int | None,
@@ -215,16 +257,19 @@ def form_list(
215
257
  \b
216
258
  Acts as you. You can only see and act on what you could in the webapp:
217
259
  by default this is every form in your org you have list-view access to.
218
- Pass --mine to narrow to only the forms you own.
219
- Archived forms are hidden unless you pass --include-archived.
260
+ Pass --mine to narrow to only the forms you own, or --filter to scope.
261
+ Archived forms are hidden unless you pass --include-archived or --filter archived.
220
262
 
221
263
  \b
222
264
  Examples:
223
265
  dailybot form list
224
266
  dailybot form list --mine
267
+ dailybot form list --filter workflow --order alphabetical --asc
268
+ dailybot form list --filter approval
269
+ dailybot form list --filter public --order total
225
270
  dailybot form list --search retro --since 2026-07-01
271
+ dailybot form list --include-questions --json
226
272
  dailybot form list --all
227
- dailybot form list --json
228
273
  """
229
274
  enforce_plan_access("form_list", json_mode=json_mode)
230
275
  try:
@@ -248,7 +293,11 @@ def form_list(
248
293
  client,
249
294
  json_mode=json_mode,
250
295
  include_archived=include_archived,
296
+ include_questions=include_questions,
251
297
  owner="me" if mine else None,
298
+ filter_scope=filter_scope,
299
+ order=order,
300
+ is_ascend=is_ascend,
252
301
  spec=spec,
253
302
  )
254
303
 
@@ -290,11 +339,50 @@ def form_get(form_uuid: str, json_mode: bool) -> None:
290
339
  )
291
340
  @click.option("--yes", "-y", "assume_yes", is_flag=True, help="Skip the confirmation prompt.")
292
341
  @click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
342
+ @click.option(
343
+ "--automation",
344
+ is_flag=True,
345
+ default=False,
346
+ help=(
347
+ "Submit as an automation. The response will appear in channel "
348
+ "notifications without any submitter name — useful when "
349
+ "forwarding third-party form submissions via integrations."
350
+ ),
351
+ )
352
+ @click.option(
353
+ "--anonymous",
354
+ is_flag=True,
355
+ default=False,
356
+ help=(
357
+ "Submit anonymously. The response will appear with a random "
358
+ "generated name instead of your real name."
359
+ ),
360
+ )
361
+ @click.option(
362
+ "--guest-name",
363
+ default=None,
364
+ help="Guest submitter name (used with --automation for third-party submissions).",
365
+ )
366
+ @click.option(
367
+ "--guest-email",
368
+ default=None,
369
+ help="Guest submitter email (used with --automation for third-party submissions).",
370
+ )
371
+ @click.option(
372
+ "--source",
373
+ default=None,
374
+ help="Provenance label for this submission (max 512 chars, e.g. 'workflow:release-pipeline').",
375
+ )
293
376
  def form_submit(
294
377
  form_uuid: str,
295
378
  content: str | None,
296
379
  assume_yes: bool,
297
380
  json_mode: bool,
381
+ automation: bool,
382
+ anonymous: bool,
383
+ guest_name: str | None,
384
+ guest_email: str | None,
385
+ source: str | None,
298
386
  ) -> None:
299
387
  """Submit a form response.
300
388
 
@@ -309,7 +397,29 @@ def form_submit(
309
397
  dailybot form submit <form_uuid>
310
398
  dailybot form submit <form_uuid> --content '{"<question_uuid>":"Yes"}'
311
399
  dailybot form submit <form_uuid> --content '{"<uuid>":"Answer"}' --yes
400
+ dailybot form submit <form_uuid> --content '{"<uuid>":"A"}' --automation
401
+ dailybot form submit <form_uuid> --content '{"<uuid>":"A"}' --anonymous
402
+ dailybot form submit <form_uuid> --content '{"<uuid>":"A"}' --automation \\
403
+ --guest-name "Release Bot" --guest-email "bot@example.com" \\
404
+ --source "workflow:production-deploy"
312
405
  """
406
+ if guest_email and not _EMAIL_RE.match(guest_email):
407
+ print_error(f"Invalid email format: {guest_email}")
408
+ raise SystemExit(1)
409
+ if source and len(source) > MAX_SUBMISSION_SOURCE_LENGTH:
410
+ print_error(
411
+ f"--source is too long ({len(source)} chars, max {MAX_SUBMISSION_SOURCE_LENGTH})."
412
+ )
413
+ raise SystemExit(1)
414
+
415
+ guest_user: dict[str, str] | None = None
416
+ if guest_name or guest_email:
417
+ guest_user = {}
418
+ if guest_name:
419
+ guest_user["full_name"] = guest_name
420
+ if guest_email:
421
+ guest_user["email"] = guest_email
422
+
313
423
  client = require_auth()
314
424
  content_map = resolve_form_content(client, form_uuid, content)
315
425
  execute_form_submit(
@@ -318,6 +428,10 @@ def form_submit(
318
428
  content_map,
319
429
  assume_yes=assume_yes,
320
430
  json_mode=json_mode,
431
+ automation=automation,
432
+ anonymous=anonymous,
433
+ guest_user=guest_user,
434
+ submission_source=source,
321
435
  )
322
436
 
323
437
 
@@ -332,15 +446,48 @@ def form_submit(
332
446
  "--all",
333
447
  "all_responses",
334
448
  is_flag=True,
335
- help="List everyone's responses (admin/owner only; a member gets 403).",
449
+ help="List everyone's responses (requires VIEW_REPORTS permission).",
336
450
  )
337
451
  @click.option("--user", default=None, help="Filter to one user's responses (admin/owner only).")
452
+ @click.option(
453
+ "--source",
454
+ "submission_sources",
455
+ default=None,
456
+ help="Filter by origin (CSV: member, anonymous, automation, public).",
457
+ )
458
+ @click.option(
459
+ "--submitter",
460
+ "submitter_ids",
461
+ default=None,
462
+ help="Filter by submitter UUIDs (CSV, max 50).",
463
+ )
464
+ @click.option(
465
+ "--flow-status",
466
+ "flow_status",
467
+ type=click.Choice(RESPONSE_FLOW_STATUSES, case_sensitive=False),
468
+ default=None,
469
+ help="Approval flow status: pending, approved, denied.",
470
+ )
471
+ @click.option(
472
+ "--order",
473
+ type=click.Choice(RESPONSE_ORDERS, case_sensitive=False),
474
+ default=None,
475
+ help="Sort order: recent or oldest.",
476
+ )
477
+ @click.option(
478
+ "--ascending",
479
+ "--asc",
480
+ "is_ascend",
481
+ is_flag=True,
482
+ default=False,
483
+ help="Sort ascending (alias for --order oldest).",
484
+ )
338
485
  @click.option(
339
486
  "--from", "date_from", default=None, help="Responses on/after this date (YYYY-MM-DD)."
340
487
  )
341
488
  @click.option("--to", "date_to", default=None, help="Responses on/before this date (YYYY-MM-DD).")
342
489
  @click.option(
343
- "--search", "-s", "search", default=None, help="Filter response content (max 256 chars)."
490
+ "--search", "-s", "search", default=None, help="Search content and submitter (max 256 chars)."
344
491
  )
345
492
  @click.option(
346
493
  "--latest",
@@ -354,6 +501,11 @@ def form_responses(
354
501
  state: str | None,
355
502
  all_responses: bool,
356
503
  user: str | None,
504
+ submission_sources: str | None,
505
+ submitter_ids: str | None,
506
+ flow_status: str | None,
507
+ order: str | None,
508
+ is_ascend: bool,
357
509
  date_from: str | None,
358
510
  date_to: str | None,
359
511
  search: str | None,
@@ -367,17 +519,45 @@ def form_responses(
367
519
 
368
520
  \b
369
521
  Acts as you. By default the server returns only responses you authored.
370
- --all / --user surface others' responses and are admin/owner-only
371
- (server-enforced — a member receives 403). --from / --to narrow the window.
522
+ --all / --user surface others' responses and require VIEW_REPORTS
523
+ permission (server-enforced — a member receives 403).
524
+
525
+ \b
526
+ --source filters by origin: member, anonymous, automation, public.
527
+ Multiple values are comma-separated (OR semantics).
528
+ --submitter filters by specific user UUIDs (CSV, max 50, OR semantics).
529
+ --flow-status filters approval status: pending, approved, denied.
372
530
 
373
531
  \b
374
532
  Examples:
375
533
  dailybot form responses <form_uuid>
376
534
  dailybot form responses <form_uuid> --state qa --json
377
535
  dailybot form responses <form_uuid> --all --from 2026-01-01 --to 2026-06-30
536
+ dailybot form responses <form_uuid> --all --source automation
537
+ dailybot form responses <form_uuid> --all --source "member,automation"
538
+ dailybot form responses <form_uuid> --all --flow-status pending
539
+ dailybot form responses <form_uuid> --all --search "deploy" --order oldest
378
540
  dailybot form responses <form_uuid> --user <user_uuid> --json
379
541
  """
380
542
  validate_user_filter(user)
543
+ if submission_sources:
544
+ parts: list[str] = [s.strip() for s in submission_sources.split(",")]
545
+ invalid: list[str] = [s for s in parts if s not in RESPONSE_SOURCES]
546
+ if invalid:
547
+ print_error(
548
+ f"Invalid source(s): {', '.join(invalid)}. Allowed: {', '.join(RESPONSE_SOURCES)}."
549
+ )
550
+ raise SystemExit(1)
551
+ if submitter_ids:
552
+ ids: list[str] = [s.strip() for s in submitter_ids.split(",")]
553
+ if len(ids) > MAX_SUBMITTER_IDS:
554
+ print_error(f"Too many submitter UUIDs ({len(ids)}, max {MAX_SUBMITTER_IDS}).")
555
+ raise SystemExit(1)
556
+
557
+ effective_order: str | None = order
558
+ if is_ascend and not order:
559
+ effective_order = "oldest"
560
+
381
561
  client = require_auth()
382
562
  truncated_search: str | None = search[:256] if search is not None else None
383
563
  meta: dict[str, Any] = {}
@@ -388,6 +568,11 @@ def form_responses(
388
568
  state=state,
389
569
  all_responses=all_responses,
390
570
  user=user,
571
+ submission_sources=submission_sources,
572
+ submitter_user_ids=submitter_ids,
573
+ flow_status=flow_status,
574
+ order=effective_order,
575
+ is_ascend=is_ascend,
391
576
  date_from=date_from,
392
577
  date_to=date_to,
393
578
  search=truncated_search,
@@ -234,6 +234,18 @@ ERROR_CODE_MESSAGES: dict[str, str] = {
234
234
  "search_query_too_long": (
235
235
  "Search term is too long (maximum 256 characters). Shorten it and try again."
236
236
  ),
237
+ "guest_user_required": (
238
+ "This form requires guest submitter info. "
239
+ "Add --guest-name and --guest-email to your command."
240
+ ),
241
+ "invalid_filter": "Unknown --filter value. Use: all, me, public, approval, workflow, archived.",
242
+ "invalid_order": "Unknown --order value. Use: alphabetical, recent, total (forms) or recent, oldest (responses).",
243
+ "invalid_submission_sources": (
244
+ "Unknown --source value. Use: member, anonymous, automation, public (comma-separated)."
245
+ ),
246
+ "invalid_submitter_user_id": "Invalid --submitter UUID format.",
247
+ "too_many_submitter_user_ids": "Too many --submitter UUIDs (max 50).",
248
+ "invalid_flow_status": "Unknown --flow-status value. Use: pending, approved, denied.",
237
249
  "invalid_date_range": (
238
250
  "Invalid date. Use YYYY-MM-DD, and make sure the start date is on or before the end date."
239
251
  ),
@@ -500,19 +500,27 @@ def execute_form_list(
500
500
  *,
501
501
  json_mode: bool = False,
502
502
  include_archived: bool = False,
503
+ include_questions: bool = False,
503
504
  owner: str | None = None,
505
+ filter_scope: str | None = None,
506
+ order: str | None = None,
507
+ is_ascend: bool = False,
504
508
  spec: QuerySpec | None = None,
505
509
  ) -> list[dict[str, Any]] | None:
506
510
  """Fetch and display forms visible to the user (with question counts)."""
507
511
  query: QuerySpec = spec or QuerySpec()
508
512
  fetch_all: bool = resolve_fetch_all(query)
509
513
  meta: dict[str, Any] = {}
514
+ always_include_questions: bool = include_questions or not json_mode
510
515
  try:
511
516
  with console.status("Fetching forms..."):
512
517
  forms: list[dict[str, Any]] = client.list_forms(
513
- include_questions=True,
518
+ include_questions=always_include_questions,
514
519
  include_archived=include_archived,
515
520
  owner=owner,
521
+ filter_scope=filter_scope,
522
+ order=order,
523
+ is_ascend=is_ascend,
516
524
  search=query.params.get("search"),
517
525
  start_date=query.params.get("start_date"),
518
526
  end_date=query.params.get("end_date"),
@@ -542,6 +550,10 @@ def execute_form_submit(
542
550
  form_name: str | None = None,
543
551
  assume_yes: bool = False,
544
552
  json_mode: bool = False,
553
+ automation: bool = False,
554
+ anonymous: bool = False,
555
+ guest_user: dict[str, str] | None = None,
556
+ submission_source: str | None = None,
545
557
  ) -> None:
546
558
  """Submit a form response."""
547
559
  resolved_name: str = form_name or form_uuid
@@ -556,8 +568,19 @@ def execute_form_submit(
556
568
  summary_lines: list[str] = [
557
569
  f"Form: {resolved_name}",
558
570
  f"Form UUID: {form_uuid}",
559
- "Answers:",
560
571
  ]
572
+ if automation:
573
+ summary_lines.append("Mode: automation (no submitter shown in channel)")
574
+ if anonymous:
575
+ summary_lines.append("Mode: anonymous (random name shown in channel)")
576
+ if guest_user:
577
+ guest_desc: str = guest_user.get("full_name", "")
578
+ if guest_user.get("email"):
579
+ guest_desc += f" <{guest_user['email']}>" if guest_desc else guest_user["email"]
580
+ summary_lines.append(f"Guest: {guest_desc}")
581
+ if submission_source:
582
+ summary_lines.append(f"Source: {submission_source}")
583
+ summary_lines.append("Answers:")
561
584
  for question_uuid, answer in content_map.items():
562
585
  summary_lines.append(f" {question_uuid}: {answer}")
563
586
 
@@ -568,6 +591,10 @@ def execute_form_submit(
568
591
  result: dict[str, Any] = client.submit_form_response(
569
592
  form_uuid=form_uuid,
570
593
  content=content_map,
594
+ automation=automation,
595
+ anonymous=anonymous,
596
+ guest_user=guest_user,
597
+ submission_source=submission_source,
571
598
  )
572
599
  except APIError as exc:
573
600
  exit_for_api_error(exc, json_mode)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dailybot-cli
3
- Version: 3.4.0
3
+ Version: 3.5.1
4
4
  Summary: Dailybot CLI - The command-line bridge between humans and agents. Progress reports, observability, and workflow automation for modern teams across Slack, Google Chat, Discord, MS Teams, and more.
5
5
  Author-email: Dailybot <support@dailybot.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dailybot-cli"
7
- version = "3.4.0"
7
+ version = "3.5.1"
8
8
  description = "Dailybot CLI - The command-line bridge between humans and agents. Progress reports, observability, and workflow automation for modern teams across Slack, Google Chat, Discord, MS Teams, and more."
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
File without changes
File without changes
File without changes