dailybot-cli 3.7.4__tar.gz → 3.8.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.
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/PKG-INFO +35 -6
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/README.md +34 -5
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/api_client.py +25 -1
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/commands/chat.py +298 -21
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/commands/public_api_helpers.py +45 -0
- dailybot_cli-3.8.0/dailybot_cli/commands/workflow.py +227 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli.egg-info/PKG-INFO +35 -6
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/pyproject.toml +1 -1
- dailybot_cli-3.7.4/dailybot_cli/commands/workflow.py +0 -129
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/LICENSE +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/__init__.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/commands/__init__.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/commands/agent.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/commands/ask.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/commands/auth.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/commands/authoring_helpers.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/commands/channels.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/commands/checkin.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/commands/config.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/commands/conversation.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/commands/env.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/commands/form.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/commands/hook.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/commands/identity.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/commands/interactive.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/commands/interactive_chat.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/commands/kudos.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/commands/query_options.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/commands/status.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/commands/team.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/commands/uninstall.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/commands/update.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/commands/upgrade.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/commands/user.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/commands/user_scoped_actions.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/commands/version.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/config.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/display.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/install_method.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/ledger.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/main.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/tui/__init__.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/tui/app.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/tui/commands.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/tui/conversation.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli/tui/intents.py +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli.egg-info/SOURCES.txt +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli.egg-info/dependency_links.txt +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli.egg-info/entry_points.txt +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli.egg-info/requires.txt +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/dailybot_cli.egg-info/top_level.txt +0 -0
- {dailybot_cli-3.7.4 → dailybot_cli-3.8.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dailybot-cli
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.8.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
|
|
@@ -534,21 +534,33 @@ The `user list` table shows **Name** and **User UUID**. You can copy a UUID dire
|
|
|
534
534
|
|
|
535
535
|
## Workflows
|
|
536
536
|
|
|
537
|
-
Browse the workflows configured in your organization
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
537
|
+
Browse the workflows configured in your organization, and **trigger** ones whose
|
|
538
|
+
trigger type is `api_trigger` ("When triggered via API or button" in the
|
|
539
|
+
automations builder). Creating and editing workflow definitions remains in the
|
|
540
|
+
Dailybot web app. The feature is plan-gated; an org on a plan without workflows
|
|
541
|
+
gets a clear upgrade message.
|
|
541
542
|
|
|
542
543
|
```bash
|
|
543
544
|
# List workflows visible to you (paginated + searchable)
|
|
544
545
|
dailybot workflow list
|
|
545
546
|
dailybot workflow list --search "release" --all --json
|
|
547
|
+
# Find triggerable workflows
|
|
548
|
+
dailybot workflow list --filter api_trigger --all
|
|
546
549
|
|
|
547
550
|
# Get a single workflow by UUID
|
|
548
551
|
dailybot workflow get <workflow_uuid>
|
|
549
552
|
dailybot workflow get <workflow_uuid> --json
|
|
553
|
+
|
|
554
|
+
# Queue an api_trigger run (async — returns 202; no run output)
|
|
555
|
+
dailybot workflow trigger <workflow-uuid>
|
|
556
|
+
dailybot workflow trigger <workflow-uuid> \
|
|
557
|
+
--payload '{"env":"production","requested_by":"release-bot"}'
|
|
550
558
|
```
|
|
551
559
|
|
|
560
|
+
The optional `--payload` must be a JSON object ≤ 8 KiB; workflow steps read it
|
|
561
|
+
as `{{trigger.body.*}}`. The same `api_trigger` workflows can also be fired from
|
|
562
|
+
a chat button via `callback_workflow` (see Chat below).
|
|
563
|
+
|
|
552
564
|
`dailybot workflow list` accepts the shared list flags — see
|
|
553
565
|
[Listing, search, and pagination](#listing-search-and-pagination).
|
|
554
566
|
|
|
@@ -908,8 +920,9 @@ Replies to agent emails land as messages retrievable via `dailybot agent message
|
|
|
908
920
|
|
|
909
921
|
| Command | Description |
|
|
910
922
|
|---------|-------------|
|
|
911
|
-
| `dailybot workflow list` | List workflows visible to you (
|
|
923
|
+
| `dailybot workflow list` | List workflows visible to you (search + pagination; `--filter api_trigger`) |
|
|
912
924
|
| `dailybot workflow get <uuid>` | Show a single workflow by UUID |
|
|
925
|
+
| `dailybot workflow trigger <uuid>` | Queue an `api_trigger` workflow run (async 202; optional `--payload`) |
|
|
913
926
|
|
|
914
927
|
### Chat (send bot messages to Slack/Teams/Discord/Google Chat)
|
|
915
928
|
|
|
@@ -946,6 +959,22 @@ dailybot chat send -c C0123 -m "Build #421 ✅" \
|
|
|
946
959
|
--bot-name "Release Bot" --bot-icon-emoji ":rocket:" \
|
|
947
960
|
--link-button "Open report::https://app.company.com/report"
|
|
948
961
|
|
|
962
|
+
# Approval flow — callback_url buttons (+ optional bearer auth on the POST)
|
|
963
|
+
dailybot chat send -u <user-uuid> -m "Deploy to prod?" \
|
|
964
|
+
--approve-button "Yes=approve" --reject-button "No=deny" \
|
|
965
|
+
--callback-url https://hooks.example.com/req42 --callback-bearer "$TOKEN"
|
|
966
|
+
|
|
967
|
+
# Fire an api_trigger workflow from a button click
|
|
968
|
+
dailybot chat send -c C0123 -m "Ready to release?" \
|
|
969
|
+
--workflow-button "Run release=<workflow-uuid>"
|
|
970
|
+
|
|
971
|
+
# Full button contract (modals, response trees, callback_prompt, …) via JSON
|
|
972
|
+
dailybot chat send -u <user-uuid> -m "Need details" --buttons '[
|
|
973
|
+
{"label":"Open","button_type":"interactive","value":"open",
|
|
974
|
+
"callback_url":"https://hooks.example.com/x",
|
|
975
|
+
"modal_body":{"title":"Notes","blocks":[
|
|
976
|
+
{"type":"input","name":"notes","label":"Notes","multiline":true}]}}]'
|
|
977
|
+
|
|
949
978
|
# Send with another user's identity, or as yourself (Slack only, admin-only)
|
|
950
979
|
dailybot chat send -c C0123 -m "Posting for the team" --send-as-user <user-uuid>
|
|
951
980
|
dailybot chat send -c C0123 -m "This one is from me" --send-as-me
|
|
@@ -495,21 +495,33 @@ The `user list` table shows **Name** and **User UUID**. You can copy a UUID dire
|
|
|
495
495
|
|
|
496
496
|
## Workflows
|
|
497
497
|
|
|
498
|
-
Browse the workflows configured in your organization
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
498
|
+
Browse the workflows configured in your organization, and **trigger** ones whose
|
|
499
|
+
trigger type is `api_trigger` ("When triggered via API or button" in the
|
|
500
|
+
automations builder). Creating and editing workflow definitions remains in the
|
|
501
|
+
Dailybot web app. The feature is plan-gated; an org on a plan without workflows
|
|
502
|
+
gets a clear upgrade message.
|
|
502
503
|
|
|
503
504
|
```bash
|
|
504
505
|
# List workflows visible to you (paginated + searchable)
|
|
505
506
|
dailybot workflow list
|
|
506
507
|
dailybot workflow list --search "release" --all --json
|
|
508
|
+
# Find triggerable workflows
|
|
509
|
+
dailybot workflow list --filter api_trigger --all
|
|
507
510
|
|
|
508
511
|
# Get a single workflow by UUID
|
|
509
512
|
dailybot workflow get <workflow_uuid>
|
|
510
513
|
dailybot workflow get <workflow_uuid> --json
|
|
514
|
+
|
|
515
|
+
# Queue an api_trigger run (async — returns 202; no run output)
|
|
516
|
+
dailybot workflow trigger <workflow-uuid>
|
|
517
|
+
dailybot workflow trigger <workflow-uuid> \
|
|
518
|
+
--payload '{"env":"production","requested_by":"release-bot"}'
|
|
511
519
|
```
|
|
512
520
|
|
|
521
|
+
The optional `--payload` must be a JSON object ≤ 8 KiB; workflow steps read it
|
|
522
|
+
as `{{trigger.body.*}}`. The same `api_trigger` workflows can also be fired from
|
|
523
|
+
a chat button via `callback_workflow` (see Chat below).
|
|
524
|
+
|
|
513
525
|
`dailybot workflow list` accepts the shared list flags — see
|
|
514
526
|
[Listing, search, and pagination](#listing-search-and-pagination).
|
|
515
527
|
|
|
@@ -869,8 +881,9 @@ Replies to agent emails land as messages retrievable via `dailybot agent message
|
|
|
869
881
|
|
|
870
882
|
| Command | Description |
|
|
871
883
|
|---------|-------------|
|
|
872
|
-
| `dailybot workflow list` | List workflows visible to you (
|
|
884
|
+
| `dailybot workflow list` | List workflows visible to you (search + pagination; `--filter api_trigger`) |
|
|
873
885
|
| `dailybot workflow get <uuid>` | Show a single workflow by UUID |
|
|
886
|
+
| `dailybot workflow trigger <uuid>` | Queue an `api_trigger` workflow run (async 202; optional `--payload`) |
|
|
874
887
|
|
|
875
888
|
### Chat (send bot messages to Slack/Teams/Discord/Google Chat)
|
|
876
889
|
|
|
@@ -907,6 +920,22 @@ dailybot chat send -c C0123 -m "Build #421 ✅" \
|
|
|
907
920
|
--bot-name "Release Bot" --bot-icon-emoji ":rocket:" \
|
|
908
921
|
--link-button "Open report::https://app.company.com/report"
|
|
909
922
|
|
|
923
|
+
# Approval flow — callback_url buttons (+ optional bearer auth on the POST)
|
|
924
|
+
dailybot chat send -u <user-uuid> -m "Deploy to prod?" \
|
|
925
|
+
--approve-button "Yes=approve" --reject-button "No=deny" \
|
|
926
|
+
--callback-url https://hooks.example.com/req42 --callback-bearer "$TOKEN"
|
|
927
|
+
|
|
928
|
+
# Fire an api_trigger workflow from a button click
|
|
929
|
+
dailybot chat send -c C0123 -m "Ready to release?" \
|
|
930
|
+
--workflow-button "Run release=<workflow-uuid>"
|
|
931
|
+
|
|
932
|
+
# Full button contract (modals, response trees, callback_prompt, …) via JSON
|
|
933
|
+
dailybot chat send -u <user-uuid> -m "Need details" --buttons '[
|
|
934
|
+
{"label":"Open","button_type":"interactive","value":"open",
|
|
935
|
+
"callback_url":"https://hooks.example.com/x",
|
|
936
|
+
"modal_body":{"title":"Notes","blocks":[
|
|
937
|
+
{"type":"input","name":"notes","label":"Notes","multiline":true}]}}]'
|
|
938
|
+
|
|
910
939
|
# Send with another user's identity, or as yourself (Slack only, admin-only)
|
|
911
940
|
dailybot chat send -c C0123 -m "Posting for the team" --send-as-user <user-uuid>
|
|
912
941
|
dailybot chat send -c C0123 -m "This one is from me" --send-as-me
|
|
@@ -1443,7 +1443,7 @@ class DailyBotClient:
|
|
|
1443
1443
|
limit: int | None = None,
|
|
1444
1444
|
meta: dict[str, Any] | None = None,
|
|
1445
1445
|
) -> list[dict[str, Any]]:
|
|
1446
|
-
"""GET /v1/workflows/ — list workflows (
|
|
1446
|
+
"""GET /v1/workflows/ — list workflows (plan-gated feature)."""
|
|
1447
1447
|
params: dict[str, Any] = {}
|
|
1448
1448
|
_merge_list_query(params, search=search, start_date=start_date, end_date=end_date)
|
|
1449
1449
|
result: PaginatedResult = self._paginated_get(
|
|
@@ -1464,6 +1464,30 @@ class DailyBotClient:
|
|
|
1464
1464
|
)
|
|
1465
1465
|
return self._handle_response(response)
|
|
1466
1466
|
|
|
1467
|
+
def trigger_workflow(
|
|
1468
|
+
self,
|
|
1469
|
+
workflow_uuid: str,
|
|
1470
|
+
*,
|
|
1471
|
+
payload: dict[str, Any] | None = None,
|
|
1472
|
+
) -> dict[str, Any]:
|
|
1473
|
+
"""POST /v1/workflows/<uuid>/trigger/ — queue an ``api_trigger`` workflow.
|
|
1474
|
+
|
|
1475
|
+
Only workflows whose trigger type is ``api_trigger`` ("When triggered via
|
|
1476
|
+
API or button") can be fired this way. The run is asynchronous — success
|
|
1477
|
+
is ``202 {queued: true, workflow_uuid, detail}`` with no run output.
|
|
1478
|
+
Optional *payload* (a JSON object ≤ 8 KiB) is exposed to workflow steps
|
|
1479
|
+
as ``{{trigger.body.*}}`` variables.
|
|
1480
|
+
"""
|
|
1481
|
+
body: dict[str, Any] = {}
|
|
1482
|
+
if payload is not None:
|
|
1483
|
+
body["payload"] = payload
|
|
1484
|
+
response: httpx.Response = self._request(
|
|
1485
|
+
"POST",
|
|
1486
|
+
f"{self.api_url}/v1/workflows/{workflow_uuid}/trigger/",
|
|
1487
|
+
json=body,
|
|
1488
|
+
)
|
|
1489
|
+
return self._handle_response(response)
|
|
1490
|
+
|
|
1467
1491
|
def list_kudos_organization(
|
|
1468
1492
|
self,
|
|
1469
1493
|
*,
|
|
@@ -28,8 +28,11 @@ import click
|
|
|
28
28
|
from dailybot_cli.api_client import APIError, DailyBotClient
|
|
29
29
|
from dailybot_cli.commands.agent import _merge_repo_metadata, _resolve_agent_context
|
|
30
30
|
from dailybot_cli.commands.public_api_helpers import (
|
|
31
|
+
EXIT_NOT_AUTHENTICATED,
|
|
32
|
+
EXIT_PERMISSION_DENIED,
|
|
31
33
|
UUID_PATTERN,
|
|
32
34
|
emit_json,
|
|
35
|
+
exit_for_api_error,
|
|
33
36
|
get_current_user_uuid,
|
|
34
37
|
)
|
|
35
38
|
from dailybot_cli.display import (
|
|
@@ -46,8 +49,32 @@ CHANNEL_TYPES: tuple[str, ...] = (
|
|
|
46
49
|
"direct_message",
|
|
47
50
|
)
|
|
48
51
|
BUTTON_SEPARATOR: str = "::"
|
|
52
|
+
ERGONOMIC_BUTTON_SEPARATOR: str = "="
|
|
49
53
|
MAX_BOT_USERNAME_CHARS: int = 80
|
|
50
54
|
MAX_THREAD_RESPONSES: int = 10
|
|
55
|
+
MAX_BUTTONS_PER_MESSAGE: int = 25
|
|
56
|
+
BUTTON_CALLBACK_KEYS: tuple[str, ...] = (
|
|
57
|
+
"callback_url",
|
|
58
|
+
"callback_form",
|
|
59
|
+
"callback_command",
|
|
60
|
+
"callback_prompt",
|
|
61
|
+
"callback_workflow",
|
|
62
|
+
)
|
|
63
|
+
BUTTON_ERROR_CODES: frozenset[str] = frozenset(
|
|
64
|
+
{
|
|
65
|
+
"button_link_and_callback_conflict",
|
|
66
|
+
"button_callback_conflict",
|
|
67
|
+
"button_callback_url_invalid",
|
|
68
|
+
"button_modal_body_invalid",
|
|
69
|
+
"button_callback_form_not_found",
|
|
70
|
+
"button_callback_command_invalid",
|
|
71
|
+
"button_callback_prompt_invalid",
|
|
72
|
+
"button_callback_workflow_not_found",
|
|
73
|
+
"button_response_invalid",
|
|
74
|
+
"button_callback_auth_invalid",
|
|
75
|
+
"buttons_count_out_of_range",
|
|
76
|
+
}
|
|
77
|
+
)
|
|
51
78
|
|
|
52
79
|
|
|
53
80
|
class ChatPayloadError(ValueError):
|
|
@@ -68,6 +95,48 @@ def _parse_button(raw: str, *, kind: str) -> tuple[str, str]:
|
|
|
68
95
|
return label, value
|
|
69
96
|
|
|
70
97
|
|
|
98
|
+
def _parse_ergonomic_button(raw: str, *, flag: str) -> tuple[str, str]:
|
|
99
|
+
"""Split a ``"Label=value"`` ergonomic button spec into ``(label, value)``."""
|
|
100
|
+
label, sep, value = raw.partition(ERGONOMIC_BUTTON_SEPARATOR)
|
|
101
|
+
label = label.strip()
|
|
102
|
+
value = value.strip()
|
|
103
|
+
if not sep or not label or not value:
|
|
104
|
+
raise ChatPayloadError(
|
|
105
|
+
f"Invalid {flag} '{raw}'. Expected 'Label{ERGONOMIC_BUTTON_SEPARATOR}value' "
|
|
106
|
+
f"(e.g. 'Yes{ERGONOMIC_BUTTON_SEPARATOR}approve')."
|
|
107
|
+
)
|
|
108
|
+
return label, value
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def validate_buttons(buttons: list[Any], *, flag_hint: str = "--buttons") -> None:
|
|
112
|
+
"""Light client-side checks; never strips unknown keys (API owns the contract).
|
|
113
|
+
|
|
114
|
+
Enforces: ≤25 buttons, each entry is an object with a non-empty ``label``,
|
|
115
|
+
and at most one of the five callback fields per button. Richer rules
|
|
116
|
+
(modal size, URL shape, recursive ``response`` trees) stay server-side.
|
|
117
|
+
"""
|
|
118
|
+
if len(buttons) > MAX_BUTTONS_PER_MESSAGE:
|
|
119
|
+
raise ChatPayloadError(
|
|
120
|
+
f"At most {MAX_BUTTONS_PER_MESSAGE} buttons are allowed per message ({flag_hint})."
|
|
121
|
+
)
|
|
122
|
+
for index, button in enumerate(buttons, start=1):
|
|
123
|
+
if not isinstance(button, dict):
|
|
124
|
+
raise ChatPayloadError(f"Button #{index} must be a JSON object ({flag_hint}).")
|
|
125
|
+
label_raw: Any = button.get("label")
|
|
126
|
+
label: str = str(label_raw).strip() if label_raw is not None else ""
|
|
127
|
+
if not label:
|
|
128
|
+
raise ChatPayloadError(f"Button #{index} is missing a required 'label' ({flag_hint}).")
|
|
129
|
+
present: list[str] = [
|
|
130
|
+
key for key in BUTTON_CALLBACK_KEYS if button.get(key) not in (None, "")
|
|
131
|
+
]
|
|
132
|
+
if len(present) > 1:
|
|
133
|
+
raise ChatPayloadError(
|
|
134
|
+
f"Button '{label}' sets more than one callback ({', '.join(present)}). "
|
|
135
|
+
"At most one of callback_url, callback_form, callback_command, "
|
|
136
|
+
"callback_prompt, or callback_workflow is allowed."
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
71
140
|
def build_chat_payload(
|
|
72
141
|
*,
|
|
73
142
|
text: str | None = None,
|
|
@@ -77,6 +146,7 @@ def build_chat_payload(
|
|
|
77
146
|
image_url: str | None = None,
|
|
78
147
|
link_buttons: list[tuple[str, str]] | None = None,
|
|
79
148
|
action_buttons: list[tuple[str, str]] | None = None,
|
|
149
|
+
extra_buttons: list[dict[str, Any]] | None = None,
|
|
80
150
|
thread: str | None = None,
|
|
81
151
|
channel_type: str | None = None,
|
|
82
152
|
bot_name: str | None = None,
|
|
@@ -95,6 +165,9 @@ def build_chat_payload(
|
|
|
95
165
|
:class:`ChatPayloadError` on invalid combinations the API would reject —
|
|
96
166
|
surfacing a friendly message before the network call.
|
|
97
167
|
|
|
168
|
+
*extra_buttons* is a list of raw button objects (from ``--buttons`` JSON or
|
|
169
|
+
ergonomic approval/workflow flags). Keys are forwarded untouched — the CLI
|
|
170
|
+
stays forward-compatible with every current and future button field.
|
|
98
171
|
*thread_responses* posts follow-up messages inside the parent's thread in
|
|
99
172
|
the same call (each reply inherits the parent's recipients). The API mints
|
|
100
173
|
one id per reply in the response, so each is independently editable.
|
|
@@ -136,7 +209,10 @@ def build_chat_payload(
|
|
|
136
209
|
buttons.append({"label": label, "button_type": "link", "url": url})
|
|
137
210
|
for label, value in action_buttons or []:
|
|
138
211
|
buttons.append({"label": label, "button_type": "interactive", "value": value})
|
|
212
|
+
if extra_buttons:
|
|
213
|
+
buttons.extend(extra_buttons)
|
|
139
214
|
if buttons:
|
|
215
|
+
validate_buttons(buttons)
|
|
140
216
|
payload["buttons"] = buttons
|
|
141
217
|
|
|
142
218
|
if users:
|
|
@@ -212,7 +288,9 @@ def _send(
|
|
|
212
288
|
client: DailyBotClient, payload: dict[str, Any], *, updated: bool, json_mode: bool
|
|
213
289
|
) -> None:
|
|
214
290
|
"""Run a single send/update call and render the result (used by `update`)."""
|
|
215
|
-
result: dict[str, Any] = _execute_send(
|
|
291
|
+
result: dict[str, Any] = _execute_send(
|
|
292
|
+
client, payload, status="Sending message...", json_mode=json_mode
|
|
293
|
+
)
|
|
216
294
|
if json_mode:
|
|
217
295
|
emit_json(result)
|
|
218
296
|
return
|
|
@@ -220,9 +298,13 @@ def _send(
|
|
|
220
298
|
|
|
221
299
|
|
|
222
300
|
def _execute_send(
|
|
223
|
-
client: DailyBotClient,
|
|
301
|
+
client: DailyBotClient,
|
|
302
|
+
payload: dict[str, Any],
|
|
303
|
+
*,
|
|
304
|
+
status: str,
|
|
305
|
+
json_mode: bool = False,
|
|
224
306
|
) -> dict[str, Any]:
|
|
225
|
-
"""Call the API with friendly error translation; return the result or exit
|
|
307
|
+
"""Call the API with friendly error translation; return the result or exit."""
|
|
226
308
|
if payload.get("platform_settings", {}).get("is_ephemeral") and not payload.get("target_users"):
|
|
227
309
|
# The API silently skips an ephemeral message with no resolvable user.
|
|
228
310
|
print_warning(
|
|
@@ -233,31 +315,50 @@ def _execute_send(
|
|
|
233
315
|
with console.status(status):
|
|
234
316
|
return client.send_chat_message(payload)
|
|
235
317
|
except APIError as e:
|
|
236
|
-
|
|
237
|
-
|
|
318
|
+
overrides: dict[str, str] = {
|
|
319
|
+
"org_admin_required": (
|
|
238
320
|
"Sending as another user (--send-as-user / --send-as-me) requires organization "
|
|
239
321
|
"admin privileges. Run it with an admin account or an org admin's API key."
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
print_error(
|
|
322
|
+
),
|
|
323
|
+
"cli_send_message_target_not_allowed": (
|
|
243
324
|
f"{e.detail}\n Your role can only reach teammates, public channels, and teams "
|
|
244
325
|
"you belong to. Use an allowed target, or an org API key for org-wide reach."
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
print_error(
|
|
326
|
+
),
|
|
327
|
+
"invalid_thread_responses": (
|
|
248
328
|
f"{e.detail}\n Thread replies allow at most 10 items, one level deep, with no "
|
|
249
329
|
"targeting of their own (they inherit the parent's recipients)."
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
|
|
330
|
+
),
|
|
331
|
+
}
|
|
332
|
+
# Button contract errors: surface the server detail verbatim (richest signal).
|
|
333
|
+
if e.code and e.code in BUTTON_ERROR_CODES and e.detail:
|
|
334
|
+
overrides[e.code] = e.detail
|
|
335
|
+
# Auth hint when the server didn't send a more specific code.
|
|
336
|
+
if e.status_code in (401, 403) and e.code not in overrides:
|
|
337
|
+
auth_hint: str = (
|
|
253
338
|
f"{e.detail}\n Authenticate first: run 'dailybot login' (sends as you) or set an "
|
|
254
339
|
"org API key with 'dailybot config key=<API_KEY>'."
|
|
255
340
|
)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
341
|
+
if e.code:
|
|
342
|
+
overrides[e.code] = auth_hint
|
|
343
|
+
else:
|
|
344
|
+
if json_mode:
|
|
345
|
+
emit_json({"error": auth_hint, "status": e.status_code, "detail": e.detail})
|
|
346
|
+
else:
|
|
347
|
+
print_error(auth_hint)
|
|
348
|
+
# Match exit_for_api_error's status→exit-code contract so headless
|
|
349
|
+
# consumers see a stable code whether or not the server sent `code`.
|
|
350
|
+
exit_code: int = (
|
|
351
|
+
EXIT_NOT_AUTHENTICATED if e.status_code == 401 else EXIT_PERMISSION_DENIED
|
|
352
|
+
)
|
|
353
|
+
raise SystemExit(exit_code)
|
|
354
|
+
if e.status_code == 429 and e.code is None:
|
|
355
|
+
rate_msg: str = "Rate limit exceeded for chat sends. Wait a bit and retry."
|
|
356
|
+
if json_mode:
|
|
357
|
+
emit_json({"error": rate_msg, "status": 429, "detail": e.detail})
|
|
358
|
+
else:
|
|
359
|
+
print_error(rate_msg)
|
|
360
|
+
raise SystemExit(1)
|
|
361
|
+
exit_for_api_error(e, json_mode, code_overrides=overrides)
|
|
261
362
|
|
|
262
363
|
|
|
263
364
|
# --- chat group ---
|
|
@@ -313,6 +414,49 @@ def _target_options(fn: Any) -> Any:
|
|
|
313
414
|
multiple=True,
|
|
314
415
|
help="Interactive button 'Label::value' (repeatable).",
|
|
315
416
|
),
|
|
417
|
+
click.option(
|
|
418
|
+
"--buttons",
|
|
419
|
+
"buttons_json",
|
|
420
|
+
default=None,
|
|
421
|
+
help=(
|
|
422
|
+
"Raw buttons JSON array — full API contract (callbacks, modals, response, "
|
|
423
|
+
"callback_auth). Keys forwarded untouched; max 25."
|
|
424
|
+
),
|
|
425
|
+
),
|
|
426
|
+
click.option(
|
|
427
|
+
"--approve-button",
|
|
428
|
+
"approve_button_raw",
|
|
429
|
+
default=None,
|
|
430
|
+
help="Approval button 'Label=value' (needs --callback-url).",
|
|
431
|
+
),
|
|
432
|
+
click.option(
|
|
433
|
+
"--reject-button",
|
|
434
|
+
"reject_button_raw",
|
|
435
|
+
default=None,
|
|
436
|
+
help="Reject button 'Label=value' (needs --callback-url).",
|
|
437
|
+
),
|
|
438
|
+
click.option(
|
|
439
|
+
"--callback-url",
|
|
440
|
+
"callback_url",
|
|
441
|
+
default=None,
|
|
442
|
+
help="HTTPS callback URL for --approve-button / --reject-button.",
|
|
443
|
+
),
|
|
444
|
+
click.option(
|
|
445
|
+
"--callback-bearer",
|
|
446
|
+
"callback_bearer",
|
|
447
|
+
default=None,
|
|
448
|
+
help=(
|
|
449
|
+
"Optional bearer token for callback_auth on approval buttons. Prefer "
|
|
450
|
+
'passing via an env var (e.g. --callback-bearer "$TOKEN") — a raw '
|
|
451
|
+
"token on the command line lands in shell history and process lists."
|
|
452
|
+
),
|
|
453
|
+
),
|
|
454
|
+
click.option(
|
|
455
|
+
"--workflow-button",
|
|
456
|
+
"workflow_buttons_raw",
|
|
457
|
+
multiple=True,
|
|
458
|
+
help="Workflow button 'Label=<workflow-uuid>' (callback_workflow; repeatable).",
|
|
459
|
+
),
|
|
316
460
|
click.option("--thread", default=None, help="Thread id to reply inside (channels)."),
|
|
317
461
|
click.option(
|
|
318
462
|
"--channel-type",
|
|
@@ -353,6 +497,75 @@ def _target_options(fn: Any) -> Any:
|
|
|
353
497
|
return fn
|
|
354
498
|
|
|
355
499
|
|
|
500
|
+
def _build_extra_buttons(
|
|
501
|
+
*,
|
|
502
|
+
buttons_json: str | None,
|
|
503
|
+
approve_button_raw: str | None,
|
|
504
|
+
reject_button_raw: str | None,
|
|
505
|
+
callback_url: str | None,
|
|
506
|
+
callback_bearer: str | None,
|
|
507
|
+
workflow_buttons_raw: tuple[str, ...],
|
|
508
|
+
) -> list[dict[str, Any]]:
|
|
509
|
+
"""Assemble raw button objects from --buttons JSON and ergonomic flags."""
|
|
510
|
+
extra: list[dict[str, Any]] = []
|
|
511
|
+
|
|
512
|
+
if buttons_json is not None:
|
|
513
|
+
try:
|
|
514
|
+
parsed: Any = json.loads(buttons_json)
|
|
515
|
+
except json.JSONDecodeError:
|
|
516
|
+
raise ChatPayloadError("Invalid JSON in --buttons.") from None
|
|
517
|
+
if not isinstance(parsed, list):
|
|
518
|
+
raise ChatPayloadError("--buttons must be a JSON array of button objects.")
|
|
519
|
+
for item in parsed:
|
|
520
|
+
if not isinstance(item, dict):
|
|
521
|
+
raise ChatPayloadError("--buttons entries must be JSON objects.")
|
|
522
|
+
extra.append(dict(item)) # shallow copy; keys untouched
|
|
523
|
+
|
|
524
|
+
if callback_bearer and not callback_url:
|
|
525
|
+
raise ChatPayloadError("--callback-bearer requires --callback-url.")
|
|
526
|
+
if (approve_button_raw or reject_button_raw) and not callback_url:
|
|
527
|
+
raise ChatPayloadError("--approve-button / --reject-button require --callback-url.")
|
|
528
|
+
if callback_url and not (approve_button_raw or reject_button_raw):
|
|
529
|
+
raise ChatPayloadError(
|
|
530
|
+
"--callback-url needs at least one of --approve-button / --reject-button "
|
|
531
|
+
"(or put callback_url inside --buttons JSON)."
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
callback_auth: dict[str, Any] | None = None
|
|
535
|
+
if callback_bearer:
|
|
536
|
+
callback_auth = {"type": "bearer", "token": callback_bearer}
|
|
537
|
+
|
|
538
|
+
for raw, flag in (
|
|
539
|
+
(approve_button_raw, "--approve-button"),
|
|
540
|
+
(reject_button_raw, "--reject-button"),
|
|
541
|
+
):
|
|
542
|
+
if not raw:
|
|
543
|
+
continue
|
|
544
|
+
label, value = _parse_ergonomic_button(raw, flag=flag)
|
|
545
|
+
button: dict[str, Any] = {
|
|
546
|
+
"label": label,
|
|
547
|
+
"button_type": "interactive",
|
|
548
|
+
"value": value,
|
|
549
|
+
"callback_url": callback_url,
|
|
550
|
+
}
|
|
551
|
+
if callback_auth is not None:
|
|
552
|
+
button["callback_auth"] = dict(callback_auth)
|
|
553
|
+
extra.append(button)
|
|
554
|
+
|
|
555
|
+
for raw in workflow_buttons_raw:
|
|
556
|
+
label, workflow_uuid = _parse_ergonomic_button(raw, flag="--workflow-button")
|
|
557
|
+
extra.append(
|
|
558
|
+
{
|
|
559
|
+
"label": label,
|
|
560
|
+
"button_type": "interactive",
|
|
561
|
+
"value": workflow_uuid,
|
|
562
|
+
"callback_workflow": workflow_uuid,
|
|
563
|
+
}
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
return extra
|
|
567
|
+
|
|
568
|
+
|
|
356
569
|
def _assemble_payload(
|
|
357
570
|
*,
|
|
358
571
|
payload_json: str | None,
|
|
@@ -391,6 +604,11 @@ def _assemble_payload(
|
|
|
391
604
|
raw["bot_message_id"] = bot_message_id
|
|
392
605
|
try:
|
|
393
606
|
_validate_targets(raw)
|
|
607
|
+
raw_buttons: Any = raw.get("buttons")
|
|
608
|
+
if raw_buttons is not None:
|
|
609
|
+
if not isinstance(raw_buttons, list):
|
|
610
|
+
raise ChatPayloadError("--payload-json 'buttons' must be a JSON array.")
|
|
611
|
+
validate_buttons(raw_buttons, flag_hint="--payload-json buttons")
|
|
394
612
|
except ChatPayloadError as exc:
|
|
395
613
|
print_error(str(exc))
|
|
396
614
|
raise SystemExit(1)
|
|
@@ -400,14 +618,29 @@ def _assemble_payload(
|
|
|
400
618
|
action_buttons: list[tuple[str, str]] = []
|
|
401
619
|
thread_messages: tuple[str, ...] = build_kwargs.pop("thread_messages_raw", ())
|
|
402
620
|
thread_responses: list[dict[str, Any]] = [{"message": t} for t in thread_messages if t]
|
|
621
|
+
buttons_json: str | None = build_kwargs.pop("buttons_json", None)
|
|
622
|
+
approve_button_raw: str | None = build_kwargs.pop("approve_button_raw", None)
|
|
623
|
+
reject_button_raw: str | None = build_kwargs.pop("reject_button_raw", None)
|
|
624
|
+
callback_url: str | None = build_kwargs.pop("callback_url", None)
|
|
625
|
+
callback_bearer: str | None = build_kwargs.pop("callback_bearer", None)
|
|
626
|
+
workflow_buttons_raw: tuple[str, ...] = build_kwargs.pop("workflow_buttons_raw", ())
|
|
403
627
|
try:
|
|
404
628
|
for raw_btn in build_kwargs.pop("link_buttons_raw", ()):
|
|
405
629
|
link_buttons.append(_parse_button(raw_btn, kind="link"))
|
|
406
630
|
for raw_btn in build_kwargs.pop("action_buttons_raw", ()):
|
|
407
631
|
action_buttons.append(_parse_button(raw_btn, kind="interactive"))
|
|
632
|
+
extra_buttons: list[dict[str, Any]] = _build_extra_buttons(
|
|
633
|
+
buttons_json=buttons_json,
|
|
634
|
+
approve_button_raw=approve_button_raw,
|
|
635
|
+
reject_button_raw=reject_button_raw,
|
|
636
|
+
callback_url=callback_url,
|
|
637
|
+
callback_bearer=callback_bearer,
|
|
638
|
+
workflow_buttons_raw=workflow_buttons_raw,
|
|
639
|
+
)
|
|
408
640
|
return build_chat_payload(
|
|
409
641
|
link_buttons=link_buttons,
|
|
410
642
|
action_buttons=action_buttons,
|
|
643
|
+
extra_buttons=extra_buttons or None,
|
|
411
644
|
metadata=metadata_dict,
|
|
412
645
|
bot_message_id=bot_message_id,
|
|
413
646
|
thread_responses=thread_responses or None,
|
|
@@ -452,6 +685,12 @@ def chat_send(
|
|
|
452
685
|
image_url: str | None,
|
|
453
686
|
link_buttons_raw: tuple[str, ...],
|
|
454
687
|
action_buttons_raw: tuple[str, ...],
|
|
688
|
+
buttons_json: str | None,
|
|
689
|
+
approve_button_raw: str | None,
|
|
690
|
+
reject_button_raw: str | None,
|
|
691
|
+
callback_url: str | None,
|
|
692
|
+
callback_bearer: str | None,
|
|
693
|
+
workflow_buttons_raw: tuple[str, ...],
|
|
455
694
|
thread: str | None,
|
|
456
695
|
channel_type: str | None,
|
|
457
696
|
bot_name: str | None,
|
|
@@ -478,6 +717,25 @@ def chat_send(
|
|
|
478
717
|
dailybot chat send -u ana@co.com -m "Heads up" --ephemeral
|
|
479
718
|
dailybot chat send --payload-json '{"target_channels":["C0"],"messages":[...]}' --json
|
|
480
719
|
|
|
720
|
+
\b
|
|
721
|
+
Approval flow (callback_url buttons + optional bearer auth):
|
|
722
|
+
dailybot chat send -u <uuid> -m "Deploy?" \\
|
|
723
|
+
--approve-button "Yes=approve" --reject-button "No=deny" \\
|
|
724
|
+
--callback-url https://hooks.example.com/req42 --callback-bearer "$TOKEN"
|
|
725
|
+
|
|
726
|
+
\b
|
|
727
|
+
Workflow button (fires an api_trigger workflow on click):
|
|
728
|
+
dailybot chat send -c C0123 -m "Ready?" \\
|
|
729
|
+
--workflow-button "Run release=<workflow-uuid>"
|
|
730
|
+
|
|
731
|
+
\b
|
|
732
|
+
Full button contract via --buttons JSON (modals, response trees, …):
|
|
733
|
+
dailybot chat send -u <uuid> -m "Details?" --buttons '[
|
|
734
|
+
{"label":"Open","button_type":"interactive","value":"open",
|
|
735
|
+
"callback_url":"https://hooks.example.com/x",
|
|
736
|
+
"modal_body":{"title":"Notes","blocks":[
|
|
737
|
+
{"type":"input","name":"notes","label":"Notes","multiline":true}]}}]'
|
|
738
|
+
|
|
481
739
|
\b
|
|
482
740
|
Report style — a short headline plus the detail inside its thread:
|
|
483
741
|
dailybot chat send -c C0123 -m "🚀 Release v2.4 shipped" \\
|
|
@@ -511,6 +769,12 @@ def chat_send(
|
|
|
511
769
|
image_url=image_url,
|
|
512
770
|
link_buttons_raw=link_buttons_raw,
|
|
513
771
|
action_buttons_raw=action_buttons_raw,
|
|
772
|
+
buttons_json=buttons_json,
|
|
773
|
+
approve_button_raw=approve_button_raw,
|
|
774
|
+
reject_button_raw=reject_button_raw,
|
|
775
|
+
callback_url=callback_url,
|
|
776
|
+
callback_bearer=callback_bearer,
|
|
777
|
+
workflow_buttons_raw=workflow_buttons_raw,
|
|
514
778
|
thread_messages_raw=thread_messages_raw,
|
|
515
779
|
thread=thread,
|
|
516
780
|
channel_type=channel_type,
|
|
@@ -538,6 +802,12 @@ def chat_update(
|
|
|
538
802
|
image_url: str | None,
|
|
539
803
|
link_buttons_raw: tuple[str, ...],
|
|
540
804
|
action_buttons_raw: tuple[str, ...],
|
|
805
|
+
buttons_json: str | None,
|
|
806
|
+
approve_button_raw: str | None,
|
|
807
|
+
reject_button_raw: str | None,
|
|
808
|
+
callback_url: str | None,
|
|
809
|
+
callback_bearer: str | None,
|
|
810
|
+
workflow_buttons_raw: tuple[str, ...],
|
|
541
811
|
thread: str | None,
|
|
542
812
|
channel_type: str | None,
|
|
543
813
|
bot_name: str | None,
|
|
@@ -554,8 +824,9 @@ def chat_update(
|
|
|
554
824
|
\b
|
|
555
825
|
dailybot chat update <bot_message_id> -c C0123 -m "Status: DONE ✅"
|
|
556
826
|
|
|
557
|
-
|
|
558
|
-
edit, so identity flags
|
|
827
|
+
Buttons round-trip on update the same way as send. Note: the chat platform
|
|
828
|
+
keeps the message's original bot name/avatar on an edit, so identity flags
|
|
829
|
+
are ignored when updating. Re-send within 72h with the same bot_message_id.
|
|
559
830
|
"""
|
|
560
831
|
profile_flag: str | None = ctx.obj.get("profile")
|
|
561
832
|
client, repo_default_metadata = _resolved_client(profile_flag)
|
|
@@ -572,6 +843,12 @@ def chat_update(
|
|
|
572
843
|
image_url=image_url,
|
|
573
844
|
link_buttons_raw=link_buttons_raw,
|
|
574
845
|
action_buttons_raw=action_buttons_raw,
|
|
846
|
+
buttons_json=buttons_json,
|
|
847
|
+
approve_button_raw=approve_button_raw,
|
|
848
|
+
reject_button_raw=reject_button_raw,
|
|
849
|
+
callback_url=callback_url,
|
|
850
|
+
callback_bearer=callback_bearer,
|
|
851
|
+
workflow_buttons_raw=workflow_buttons_raw,
|
|
575
852
|
thread=thread,
|
|
576
853
|
channel_type=channel_type,
|
|
577
854
|
bot_name=bot_name,
|