dailybot-cli 3.8.0__tar.gz → 3.9.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.8.0 → dailybot_cli-3.9.0}/PKG-INFO +1 -1
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/api_client.py +209 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/checkin.py +3 -2
- dailybot_cli-3.9.0/dailybot_cli/commands/featured.py +147 -0
- dailybot_cli-3.9.0/dailybot_cli/commands/label.py +404 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/public_api_helpers.py +23 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/user_scoped_actions.py +2 -1
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/display.py +47 -2
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/main.py +4 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli.egg-info/PKG-INFO +1 -1
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli.egg-info/SOURCES.txt +2 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/pyproject.toml +1 -1
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/LICENSE +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/README.md +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/__init__.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/__init__.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/agent.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/ask.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/auth.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/authoring_helpers.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/channels.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/chat.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/config.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/conversation.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/env.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/form.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/hook.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/identity.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/interactive.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/interactive_chat.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/kudos.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/query_options.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/status.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/team.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/uninstall.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/update.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/upgrade.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/user.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/version.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/workflow.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/config.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/install_method.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/ledger.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/tui/__init__.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/tui/app.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/tui/commands.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/tui/conversation.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/tui/intents.py +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli.egg-info/dependency_links.txt +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli.egg-info/entry_points.txt +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli.egg-info/requires.txt +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli.egg-info/top_level.txt +0 -0
- {dailybot_cli-3.8.0 → dailybot_cli-3.9.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.9.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
|
|
@@ -106,6 +106,37 @@ def _merge_list_query(
|
|
|
106
106
|
return params
|
|
107
107
|
|
|
108
108
|
|
|
109
|
+
def _merge_dashboard_enrichment_query(
|
|
110
|
+
params: dict[str, Any],
|
|
111
|
+
*,
|
|
112
|
+
labels: list[str] | None = None,
|
|
113
|
+
featured: bool | None = None,
|
|
114
|
+
prioritize_featured: bool | None = None,
|
|
115
|
+
) -> dict[str, Any]:
|
|
116
|
+
"""Merge Labels / Featured dashboard enrichment query params."""
|
|
117
|
+
if labels:
|
|
118
|
+
params["labels"] = ",".join(labels)
|
|
119
|
+
if featured is not None:
|
|
120
|
+
params["featured"] = "true" if featured else "false"
|
|
121
|
+
if prioritize_featured is not None:
|
|
122
|
+
params["prioritize_featured"] = "true" if prioritize_featured else "false"
|
|
123
|
+
return params
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _label_entity_collection(entity_type: str) -> str:
|
|
127
|
+
"""Map CLI entity type (including web 'automations') to the public API path."""
|
|
128
|
+
normalized: str = entity_type.strip().lower()
|
|
129
|
+
if normalized in {"automations", "workflows", "workflow"}:
|
|
130
|
+
return "workflows"
|
|
131
|
+
if normalized in {"forms", "form"}:
|
|
132
|
+
return "forms"
|
|
133
|
+
if normalized in {"checkins", "checkin", "check-ins"}:
|
|
134
|
+
return "checkins"
|
|
135
|
+
raise ValueError(
|
|
136
|
+
f"entity type must be one of: forms, checkins, workflows (got {entity_type!r})"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
109
140
|
def _fill_meta(meta: dict[str, Any] | None, result: "PaginatedResult") -> None:
|
|
110
141
|
"""Populate a caller-provided meta dict with pagination totals, if given."""
|
|
111
142
|
if meta is not None:
|
|
@@ -640,6 +671,9 @@ class DailyBotClient:
|
|
|
640
671
|
fetch_all: bool = True,
|
|
641
672
|
limit: int | None = None,
|
|
642
673
|
meta: dict[str, Any] | None = None,
|
|
674
|
+
labels: list[str] | None = None,
|
|
675
|
+
featured: bool | None = None,
|
|
676
|
+
prioritize_featured: bool | None = None,
|
|
643
677
|
) -> list[dict[str, Any]]:
|
|
644
678
|
"""GET /v1/checkins/ — fetch visible check-ins with optional search/paging."""
|
|
645
679
|
params: dict[str, Any] = {}
|
|
@@ -652,6 +686,12 @@ class DailyBotClient:
|
|
|
652
686
|
if include_archived:
|
|
653
687
|
params["include_archived"] = "true"
|
|
654
688
|
_merge_list_query(params, search=search, start_date=start_date, end_date=end_date)
|
|
689
|
+
_merge_dashboard_enrichment_query(
|
|
690
|
+
params,
|
|
691
|
+
labels=labels,
|
|
692
|
+
featured=featured,
|
|
693
|
+
prioritize_featured=prioritize_featured,
|
|
694
|
+
)
|
|
655
695
|
result: PaginatedResult = self._paginated_get(
|
|
656
696
|
f"{self.api_url}/v1/checkins/",
|
|
657
697
|
params=params,
|
|
@@ -963,6 +1003,9 @@ class DailyBotClient:
|
|
|
963
1003
|
fetch_all: bool = True,
|
|
964
1004
|
limit: int | None = None,
|
|
965
1005
|
meta: dict[str, Any] | None = None,
|
|
1006
|
+
labels: list[str] | None = None,
|
|
1007
|
+
featured: bool | None = None,
|
|
1008
|
+
prioritize_featured: bool | None = None,
|
|
966
1009
|
) -> list[dict[str, Any]]:
|
|
967
1010
|
"""GET /v1/forms/ — optionally expand questions, search, and page.
|
|
968
1011
|
|
|
@@ -990,6 +1033,12 @@ class DailyBotClient:
|
|
|
990
1033
|
if is_ascend:
|
|
991
1034
|
params["is_ascend"] = "true"
|
|
992
1035
|
_merge_list_query(params, search=search, start_date=start_date, end_date=end_date)
|
|
1036
|
+
_merge_dashboard_enrichment_query(
|
|
1037
|
+
params,
|
|
1038
|
+
labels=labels,
|
|
1039
|
+
featured=featured,
|
|
1040
|
+
prioritize_featured=prioritize_featured,
|
|
1041
|
+
)
|
|
993
1042
|
result: PaginatedResult = self._paginated_get(
|
|
994
1043
|
f"{self.api_url}/v1/forms/",
|
|
995
1044
|
params=params,
|
|
@@ -1442,10 +1491,19 @@ class DailyBotClient:
|
|
|
1442
1491
|
fetch_all: bool = True,
|
|
1443
1492
|
limit: int | None = None,
|
|
1444
1493
|
meta: dict[str, Any] | None = None,
|
|
1494
|
+
labels: list[str] | None = None,
|
|
1495
|
+
featured: bool | None = None,
|
|
1496
|
+
prioritize_featured: bool | None = None,
|
|
1445
1497
|
) -> list[dict[str, Any]]:
|
|
1446
1498
|
"""GET /v1/workflows/ — list workflows (plan-gated feature)."""
|
|
1447
1499
|
params: dict[str, Any] = {}
|
|
1448
1500
|
_merge_list_query(params, search=search, start_date=start_date, end_date=end_date)
|
|
1501
|
+
_merge_dashboard_enrichment_query(
|
|
1502
|
+
params,
|
|
1503
|
+
labels=labels,
|
|
1504
|
+
featured=featured,
|
|
1505
|
+
prioritize_featured=prioritize_featured,
|
|
1506
|
+
)
|
|
1449
1507
|
result: PaginatedResult = self._paginated_get(
|
|
1450
1508
|
f"{self.api_url}/v1/workflows/",
|
|
1451
1509
|
params=params,
|
|
@@ -1789,6 +1847,157 @@ class DailyBotClient:
|
|
|
1789
1847
|
)
|
|
1790
1848
|
return self._handle_response(response)
|
|
1791
1849
|
|
|
1850
|
+
# --- Organization Labels (/v1/labels/) ---
|
|
1851
|
+
|
|
1852
|
+
def get_labels_entitlement(self) -> dict[str, Any]:
|
|
1853
|
+
"""GET /v1/labels/entitlement/ — org Labels feature flags for the caller."""
|
|
1854
|
+
response: httpx.Response = self._request("GET", f"{self.api_url}/v1/labels/entitlement/")
|
|
1855
|
+
return self._handle_response(response)
|
|
1856
|
+
|
|
1857
|
+
def list_labels(
|
|
1858
|
+
self,
|
|
1859
|
+
*,
|
|
1860
|
+
search: str | None = None,
|
|
1861
|
+
is_archived: bool = False,
|
|
1862
|
+
limit: int = 20,
|
|
1863
|
+
offset: int = 0,
|
|
1864
|
+
) -> dict[str, Any]:
|
|
1865
|
+
"""GET /v1/labels/ — paginated org Labels (limit/offset)."""
|
|
1866
|
+
params: dict[str, Any] = {
|
|
1867
|
+
"limit": max(1, min(limit, 100)),
|
|
1868
|
+
"offset": max(0, offset),
|
|
1869
|
+
"is_archived": is_archived,
|
|
1870
|
+
}
|
|
1871
|
+
if search:
|
|
1872
|
+
params["search"] = search
|
|
1873
|
+
response: httpx.Response = self._request("GET", f"{self.api_url}/v1/labels/", params=params)
|
|
1874
|
+
return self._handle_response(response)
|
|
1875
|
+
|
|
1876
|
+
def get_label(self, label_uuid: str) -> dict[str, Any]:
|
|
1877
|
+
"""GET /v1/labels/<uuid>/ — one organization Label."""
|
|
1878
|
+
response: httpx.Response = self._request("GET", f"{self.api_url}/v1/labels/{label_uuid}/")
|
|
1879
|
+
return self._handle_response(response)
|
|
1880
|
+
|
|
1881
|
+
def create_label(
|
|
1882
|
+
self,
|
|
1883
|
+
*,
|
|
1884
|
+
name: str,
|
|
1885
|
+
color: str | None = None,
|
|
1886
|
+
description: str | None = None,
|
|
1887
|
+
) -> dict[str, Any]:
|
|
1888
|
+
"""POST /v1/labels/ — create an organization Label."""
|
|
1889
|
+
body: dict[str, Any] = {"name": name}
|
|
1890
|
+
if color is not None:
|
|
1891
|
+
body["color"] = color
|
|
1892
|
+
if description is not None:
|
|
1893
|
+
body["description"] = description
|
|
1894
|
+
response: httpx.Response = self._request("POST", f"{self.api_url}/v1/labels/", json=body)
|
|
1895
|
+
return self._handle_response(response)
|
|
1896
|
+
|
|
1897
|
+
def update_label(self, label_uuid: str, body: dict[str, Any]) -> dict[str, Any]:
|
|
1898
|
+
"""PATCH /v1/labels/<uuid>/ — update an organization Label."""
|
|
1899
|
+
response: httpx.Response = self._request(
|
|
1900
|
+
"PATCH", f"{self.api_url}/v1/labels/{label_uuid}/", json=body
|
|
1901
|
+
)
|
|
1902
|
+
return self._handle_response(response)
|
|
1903
|
+
|
|
1904
|
+
def delete_label(self, label_uuid: str) -> None:
|
|
1905
|
+
"""DELETE /v1/labels/<uuid>/ — hard-delete (elevated only)."""
|
|
1906
|
+
response: httpx.Response = self._request(
|
|
1907
|
+
"DELETE", f"{self.api_url}/v1/labels/{label_uuid}/"
|
|
1908
|
+
)
|
|
1909
|
+
if response.status_code == 204:
|
|
1910
|
+
return
|
|
1911
|
+
self._handle_response(response)
|
|
1912
|
+
|
|
1913
|
+
def archive_label(self, label_uuid: str) -> dict[str, Any]:
|
|
1914
|
+
"""POST /v1/labels/<uuid>/archive/ — archive a Label."""
|
|
1915
|
+
response: httpx.Response = self._request(
|
|
1916
|
+
"POST", f"{self.api_url}/v1/labels/{label_uuid}/archive/"
|
|
1917
|
+
)
|
|
1918
|
+
return self._handle_response(response)
|
|
1919
|
+
|
|
1920
|
+
def assign_entity_labels(
|
|
1921
|
+
self,
|
|
1922
|
+
entity_type: str,
|
|
1923
|
+
entity_uuid: str,
|
|
1924
|
+
label_uuids: list[str],
|
|
1925
|
+
) -> dict[str, Any]:
|
|
1926
|
+
"""POST /v1/{forms|checkins|workflows}/{uuid}/labels/ — replace-set Labels."""
|
|
1927
|
+
collection: str = _label_entity_collection(entity_type)
|
|
1928
|
+
response: httpx.Response = self._request(
|
|
1929
|
+
"POST",
|
|
1930
|
+
f"{self.api_url}/v1/{collection}/{entity_uuid}/labels/",
|
|
1931
|
+
json={"label_uuids": label_uuids},
|
|
1932
|
+
)
|
|
1933
|
+
return self._handle_response(response)
|
|
1934
|
+
|
|
1935
|
+
def batch_entity_labels(
|
|
1936
|
+
self,
|
|
1937
|
+
*,
|
|
1938
|
+
entity_type: str,
|
|
1939
|
+
entity_uuids: list[str],
|
|
1940
|
+
label_uuids: list[str],
|
|
1941
|
+
mode: str,
|
|
1942
|
+
) -> dict[str, Any]:
|
|
1943
|
+
"""POST /v1/{forms|checkins|workflows}/labels/batch/ — add/remove/replace."""
|
|
1944
|
+
collection: str = _label_entity_collection(entity_type)
|
|
1945
|
+
response: httpx.Response = self._request(
|
|
1946
|
+
"POST",
|
|
1947
|
+
f"{self.api_url}/v1/{collection}/labels/batch/",
|
|
1948
|
+
json={
|
|
1949
|
+
"entity_uuids": entity_uuids,
|
|
1950
|
+
"label_uuids": label_uuids,
|
|
1951
|
+
"mode": mode,
|
|
1952
|
+
},
|
|
1953
|
+
)
|
|
1954
|
+
return self._handle_response(response)
|
|
1955
|
+
|
|
1956
|
+
# --- Private Featured stars (/v1/me/featured/) ---
|
|
1957
|
+
|
|
1958
|
+
def list_featured(self, *, entity_type: str) -> dict[str, Any]:
|
|
1959
|
+
"""GET /v1/me/featured/?entity_type= — list Featured entity UUIDs for the caller."""
|
|
1960
|
+
response: httpx.Response = self._request(
|
|
1961
|
+
"GET",
|
|
1962
|
+
f"{self.api_url}/v1/me/featured/",
|
|
1963
|
+
params={"entity_type": entity_type},
|
|
1964
|
+
)
|
|
1965
|
+
return self._handle_response(response)
|
|
1966
|
+
|
|
1967
|
+
def set_featured(
|
|
1968
|
+
self,
|
|
1969
|
+
entity_type: str,
|
|
1970
|
+
entity_uuid: str,
|
|
1971
|
+
*,
|
|
1972
|
+
featured: bool,
|
|
1973
|
+
) -> dict[str, Any]:
|
|
1974
|
+
"""PUT /v1/me/featured/{entity_type}/{uuid}/ — toggle Featured for one entity."""
|
|
1975
|
+
response: httpx.Response = self._request(
|
|
1976
|
+
"PUT",
|
|
1977
|
+
f"{self.api_url}/v1/me/featured/{entity_type}/{entity_uuid}/",
|
|
1978
|
+
json={"featured": featured},
|
|
1979
|
+
)
|
|
1980
|
+
return self._handle_response(response)
|
|
1981
|
+
|
|
1982
|
+
def batch_featured(
|
|
1983
|
+
self,
|
|
1984
|
+
*,
|
|
1985
|
+
entity_type: str,
|
|
1986
|
+
entity_uuids: list[str],
|
|
1987
|
+
featured: bool,
|
|
1988
|
+
) -> dict[str, Any]:
|
|
1989
|
+
"""POST /v1/me/featured/batch/ — batch feature/unfeature entities."""
|
|
1990
|
+
response: httpx.Response = self._request(
|
|
1991
|
+
"POST",
|
|
1992
|
+
f"{self.api_url}/v1/me/featured/batch/",
|
|
1993
|
+
json={
|
|
1994
|
+
"entity_type": entity_type,
|
|
1995
|
+
"entity_uuids": entity_uuids,
|
|
1996
|
+
"featured": featured,
|
|
1997
|
+
},
|
|
1998
|
+
)
|
|
1999
|
+
return self._handle_response(response)
|
|
2000
|
+
|
|
1792
2001
|
# --- Agent registration endpoints ---
|
|
1793
2002
|
|
|
1794
2003
|
def get_registration_challenge(self) -> dict[str, Any]:
|
|
@@ -30,6 +30,7 @@ from dailybot_cli.commands.public_api_helpers import (
|
|
|
30
30
|
emit_json,
|
|
31
31
|
enforce_plan_access,
|
|
32
32
|
exit_for_api_error,
|
|
33
|
+
normalize_checkin_entity_json,
|
|
33
34
|
require_auth,
|
|
34
35
|
validate_user_filter,
|
|
35
36
|
)
|
|
@@ -524,7 +525,7 @@ def checkin_create(
|
|
|
524
525
|
exit_for_api_error(exc, json_mode)
|
|
525
526
|
|
|
526
527
|
if json_mode:
|
|
527
|
-
emit_json(result)
|
|
528
|
+
emit_json(normalize_checkin_entity_json(result))
|
|
528
529
|
return
|
|
529
530
|
print_checkin_created(result)
|
|
530
531
|
|
|
@@ -610,7 +611,7 @@ def checkin_config(
|
|
|
610
611
|
exit_for_api_error(exc, json_mode)
|
|
611
612
|
|
|
612
613
|
if json_mode:
|
|
613
|
-
emit_json(result)
|
|
614
|
+
emit_json(normalize_checkin_entity_json(result))
|
|
614
615
|
return
|
|
615
616
|
print_success(f"Check-in {followup_uuid} updated.")
|
|
616
617
|
print_checkin_created(result, updated=True)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Private Featured star commands (/v1/me/featured/)."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from dailybot_cli.api_client import APIError
|
|
8
|
+
from dailybot_cli.commands.public_api_helpers import (
|
|
9
|
+
emit_json,
|
|
10
|
+
exit_for_api_error,
|
|
11
|
+
require_auth,
|
|
12
|
+
)
|
|
13
|
+
from dailybot_cli.display import console, print_featured_summary, print_success
|
|
14
|
+
|
|
15
|
+
FEATURED_ENTITY_TYPES: tuple[str, ...] = ("forms", "automations", "checkins")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _parse_entity_type(ctx: click.Context, param: click.Parameter, value: str) -> str:
|
|
19
|
+
normalized: str = value.strip().lower()
|
|
20
|
+
if normalized not in FEATURED_ENTITY_TYPES:
|
|
21
|
+
raise click.BadParameter(f"entity_type must be one of: {', '.join(FEATURED_ENTITY_TYPES)}")
|
|
22
|
+
return normalized
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@click.group()
|
|
26
|
+
def featured() -> None:
|
|
27
|
+
"""Manage your private Featured stars on Forms, Automations, and Check-ins.
|
|
28
|
+
|
|
29
|
+
\b
|
|
30
|
+
Featured is per-user (not org-shared). Not gated by paid Labels.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@featured.command("list")
|
|
35
|
+
@click.option(
|
|
36
|
+
"--entity-type",
|
|
37
|
+
"entity_type",
|
|
38
|
+
required=True,
|
|
39
|
+
callback=_parse_entity_type,
|
|
40
|
+
help="Entity type: forms, automations, or checkins.",
|
|
41
|
+
)
|
|
42
|
+
@click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
|
|
43
|
+
def featured_list(entity_type: str, json_mode: bool) -> None:
|
|
44
|
+
"""List Featured entity UUIDs for one entity type."""
|
|
45
|
+
client = require_auth()
|
|
46
|
+
try:
|
|
47
|
+
with console.status("Fetching featured items..."):
|
|
48
|
+
data: dict[str, Any] = client.list_featured(entity_type=entity_type)
|
|
49
|
+
except APIError as exc:
|
|
50
|
+
exit_for_api_error(exc, json_mode)
|
|
51
|
+
|
|
52
|
+
if json_mode:
|
|
53
|
+
emit_json(data)
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
print_featured_summary(data)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@featured.command("set")
|
|
60
|
+
@click.option(
|
|
61
|
+
"--entity-type",
|
|
62
|
+
"entity_type",
|
|
63
|
+
required=True,
|
|
64
|
+
callback=_parse_entity_type,
|
|
65
|
+
help="Entity type: forms, automations, or checkins.",
|
|
66
|
+
)
|
|
67
|
+
@click.argument("entity_uuid")
|
|
68
|
+
@click.option(
|
|
69
|
+
"--featured/--unfeatured",
|
|
70
|
+
default=True,
|
|
71
|
+
help="Star (default) or unstar the entity.",
|
|
72
|
+
)
|
|
73
|
+
@click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
|
|
74
|
+
def featured_set(
|
|
75
|
+
entity_type: str,
|
|
76
|
+
entity_uuid: str,
|
|
77
|
+
featured: bool,
|
|
78
|
+
json_mode: bool,
|
|
79
|
+
) -> None:
|
|
80
|
+
"""Toggle Featured for one entity."""
|
|
81
|
+
client = require_auth()
|
|
82
|
+
try:
|
|
83
|
+
with console.status("Updating featured state..."):
|
|
84
|
+
data: dict[str, Any] = client.set_featured(
|
|
85
|
+
entity_type,
|
|
86
|
+
entity_uuid,
|
|
87
|
+
featured=featured,
|
|
88
|
+
)
|
|
89
|
+
except APIError as exc:
|
|
90
|
+
exit_for_api_error(exc, json_mode)
|
|
91
|
+
|
|
92
|
+
if json_mode:
|
|
93
|
+
emit_json(data)
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
state: str = "featured" if data.get("is_featured", featured) else "unfeatured"
|
|
97
|
+
print_success(f"Marked {entity_uuid} as {state} ({entity_type}).")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@featured.command("batch")
|
|
101
|
+
@click.option(
|
|
102
|
+
"--entity-type",
|
|
103
|
+
"entity_type",
|
|
104
|
+
required=True,
|
|
105
|
+
callback=_parse_entity_type,
|
|
106
|
+
help="Entity type: forms, automations, or checkins.",
|
|
107
|
+
)
|
|
108
|
+
@click.option(
|
|
109
|
+
"--uuids",
|
|
110
|
+
required=True,
|
|
111
|
+
help="Comma-separated entity UUIDs.",
|
|
112
|
+
)
|
|
113
|
+
@click.option(
|
|
114
|
+
"--featured/--unfeatured",
|
|
115
|
+
default=True,
|
|
116
|
+
help="Feature (default) or unfeature all listed entities.",
|
|
117
|
+
)
|
|
118
|
+
@click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
|
|
119
|
+
def featured_batch(
|
|
120
|
+
entity_type: str,
|
|
121
|
+
uuids: str,
|
|
122
|
+
featured: bool,
|
|
123
|
+
json_mode: bool,
|
|
124
|
+
) -> None:
|
|
125
|
+
"""Batch feature or unfeature multiple entities."""
|
|
126
|
+
entity_uuids: list[str] = [part.strip() for part in uuids.split(",") if part.strip()]
|
|
127
|
+
if not entity_uuids:
|
|
128
|
+
raise click.UsageError("--uuids must contain at least one UUID.")
|
|
129
|
+
|
|
130
|
+
client = require_auth()
|
|
131
|
+
try:
|
|
132
|
+
with console.status("Updating featured items..."):
|
|
133
|
+
data: dict[str, Any] = client.batch_featured(
|
|
134
|
+
entity_type=entity_type,
|
|
135
|
+
entity_uuids=entity_uuids,
|
|
136
|
+
featured=featured,
|
|
137
|
+
)
|
|
138
|
+
except APIError as exc:
|
|
139
|
+
exit_for_api_error(exc, json_mode)
|
|
140
|
+
|
|
141
|
+
if json_mode:
|
|
142
|
+
emit_json(data)
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
updated: int = len(data.get("updated_entity_uuids", entity_uuids))
|
|
146
|
+
state: str = "featured" if featured else "unfeatured"
|
|
147
|
+
print_success(f"Marked {updated} {entity_type} item(s) as {state}.")
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"""Organization Labels commands (/v1/labels/)."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from dailybot_cli.api_client import APIError, DailyBotClient
|
|
8
|
+
from dailybot_cli.commands.public_api_helpers import (
|
|
9
|
+
emit_json,
|
|
10
|
+
enforce_plan_access,
|
|
11
|
+
exit_for_api_error,
|
|
12
|
+
require_auth,
|
|
13
|
+
)
|
|
14
|
+
from dailybot_cli.display import (
|
|
15
|
+
console,
|
|
16
|
+
print_detail_panel,
|
|
17
|
+
print_label_assignment,
|
|
18
|
+
print_labels_table,
|
|
19
|
+
print_success,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
_LABEL_ENTITY_TYPES: tuple[str, ...] = ("forms", "checkins", "workflows", "automations")
|
|
23
|
+
_BATCH_MODES: tuple[str, ...] = ("add", "remove", "replace")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _parse_label_entity_type(_ctx: click.Context, _param: click.Parameter, value: str) -> str:
|
|
27
|
+
normalized: str = value.strip().lower()
|
|
28
|
+
if normalized not in _LABEL_ENTITY_TYPES:
|
|
29
|
+
raise click.BadParameter(
|
|
30
|
+
"must be one of: forms, checkins, workflows (automations is an alias)"
|
|
31
|
+
)
|
|
32
|
+
if normalized == "automations":
|
|
33
|
+
return "workflows"
|
|
34
|
+
return normalized
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _parse_label_uuid_list(values: tuple[str, ...]) -> list[str]:
|
|
38
|
+
uuids: list[str] = []
|
|
39
|
+
for raw in values:
|
|
40
|
+
for part in raw.split(","):
|
|
41
|
+
token: str = part.strip()
|
|
42
|
+
if token:
|
|
43
|
+
uuids.append(token)
|
|
44
|
+
# Preserve order, drop duplicates.
|
|
45
|
+
return list(dict.fromkeys(uuids))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
_LABEL_FIELDS: list[tuple[str, str]] = [
|
|
49
|
+
("Name", "name"),
|
|
50
|
+
("UUID", "uuid"),
|
|
51
|
+
("Color", "color"),
|
|
52
|
+
("Description", "description"),
|
|
53
|
+
("Archived", "is_archived"),
|
|
54
|
+
("Usage (total)", "usage_total"),
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _usage_total(label: dict[str, Any]) -> dict[str, Any]:
|
|
59
|
+
usage: dict[str, Any] = label.get("usage") or {}
|
|
60
|
+
enriched: dict[str, Any] = dict(label)
|
|
61
|
+
enriched["usage_total"] = usage.get("total", 0)
|
|
62
|
+
return enriched
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@click.group()
|
|
66
|
+
def label() -> None:
|
|
67
|
+
"""Manage organization Labels (paid Feature.LABELS).
|
|
68
|
+
|
|
69
|
+
\b
|
|
70
|
+
Shared taxonomy for Forms, Automations, and Check-ins. Acts as you —
|
|
71
|
+
permissions match the web app (members create; elevated roles manage all).
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@label.command("entitlement")
|
|
76
|
+
@click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
|
|
77
|
+
def label_entitlement(json_mode: bool) -> None:
|
|
78
|
+
"""Show Labels entitlement flags for your org."""
|
|
79
|
+
enforce_plan_access("label_entitlement", json_mode=json_mode)
|
|
80
|
+
client = require_auth()
|
|
81
|
+
try:
|
|
82
|
+
with console.status("Checking Labels entitlement..."):
|
|
83
|
+
data: dict[str, Any] = client.get_labels_entitlement()
|
|
84
|
+
except APIError as exc:
|
|
85
|
+
exit_for_api_error(exc, json_mode)
|
|
86
|
+
|
|
87
|
+
if json_mode:
|
|
88
|
+
emit_json(data)
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
print_detail_panel(
|
|
92
|
+
"Labels entitlement",
|
|
93
|
+
data,
|
|
94
|
+
[
|
|
95
|
+
("Entitled", "entitled"),
|
|
96
|
+
("Can create", "can_create"),
|
|
97
|
+
("Can manage all", "can_manage_all"),
|
|
98
|
+
("Can hard delete", "can_hard_delete"),
|
|
99
|
+
("Guest", "is_guest"),
|
|
100
|
+
],
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@label.command("list")
|
|
105
|
+
@click.option("--search", default=None, help="Case-insensitive name search.")
|
|
106
|
+
@click.option("--archived", is_flag=True, help="Include archived Labels.")
|
|
107
|
+
@click.option("--limit", default=20, type=click.IntRange(1, 100), show_default=True)
|
|
108
|
+
@click.option("--offset", default=0, type=click.IntRange(0, 100000), show_default=True)
|
|
109
|
+
@click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
|
|
110
|
+
def label_list(
|
|
111
|
+
search: str | None,
|
|
112
|
+
archived: bool,
|
|
113
|
+
limit: int,
|
|
114
|
+
offset: int,
|
|
115
|
+
json_mode: bool,
|
|
116
|
+
) -> None:
|
|
117
|
+
"""List organization Labels."""
|
|
118
|
+
enforce_plan_access("label_list", json_mode=json_mode)
|
|
119
|
+
client = require_auth()
|
|
120
|
+
try:
|
|
121
|
+
with console.status("Fetching labels..."):
|
|
122
|
+
data: dict[str, Any] = client.list_labels(
|
|
123
|
+
search=search,
|
|
124
|
+
is_archived=archived,
|
|
125
|
+
limit=limit,
|
|
126
|
+
offset=offset,
|
|
127
|
+
)
|
|
128
|
+
except APIError as exc:
|
|
129
|
+
exit_for_api_error(exc, json_mode)
|
|
130
|
+
|
|
131
|
+
if json_mode:
|
|
132
|
+
emit_json(data)
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
results: list[dict[str, Any]] = data.get("results", [])
|
|
136
|
+
print_labels_table(results)
|
|
137
|
+
count: int | None = data.get("count")
|
|
138
|
+
if count is not None:
|
|
139
|
+
console.print(f"[dim]{count} total[/dim]")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@label.command("get")
|
|
143
|
+
@click.argument("label_uuid")
|
|
144
|
+
@click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
|
|
145
|
+
def label_get(label_uuid: str, json_mode: bool) -> None:
|
|
146
|
+
"""Get one Label by UUID."""
|
|
147
|
+
enforce_plan_access("label_get", json_mode=json_mode)
|
|
148
|
+
client = require_auth()
|
|
149
|
+
try:
|
|
150
|
+
with console.status("Loading label..."):
|
|
151
|
+
data: dict[str, Any] = client.get_label(label_uuid)
|
|
152
|
+
except APIError as exc:
|
|
153
|
+
exit_for_api_error(exc, json_mode)
|
|
154
|
+
|
|
155
|
+
if json_mode:
|
|
156
|
+
emit_json(data)
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
print_detail_panel("Label", _usage_total(data), _LABEL_FIELDS)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@label.command("create")
|
|
163
|
+
@click.option("--name", required=True, help="Label name (unique per org).")
|
|
164
|
+
@click.option("--color", default=None, help="Hex color (e.g. #4A90E2).")
|
|
165
|
+
@click.option("--description", default=None, help="Optional description.")
|
|
166
|
+
@click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
|
|
167
|
+
def label_create(
|
|
168
|
+
name: str,
|
|
169
|
+
color: str | None,
|
|
170
|
+
description: str | None,
|
|
171
|
+
json_mode: bool,
|
|
172
|
+
) -> None:
|
|
173
|
+
"""Create an organization Label."""
|
|
174
|
+
enforce_plan_access("label_create", json_mode=json_mode)
|
|
175
|
+
client = require_auth()
|
|
176
|
+
try:
|
|
177
|
+
with console.status("Creating label..."):
|
|
178
|
+
data: dict[str, Any] = client.create_label(
|
|
179
|
+
name=name,
|
|
180
|
+
color=color,
|
|
181
|
+
description=description,
|
|
182
|
+
)
|
|
183
|
+
except APIError as exc:
|
|
184
|
+
exit_for_api_error(exc, json_mode)
|
|
185
|
+
|
|
186
|
+
if json_mode:
|
|
187
|
+
emit_json(data)
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
print_success(f"Created label '{data.get('name', name)}' ({data.get('uuid', '')}).")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@label.command("update")
|
|
194
|
+
@click.argument("label_uuid")
|
|
195
|
+
@click.option("--name", default=None, help="New name.")
|
|
196
|
+
@click.option("--color", default=None, help="New hex color.")
|
|
197
|
+
@click.option("--description", default=None, help="New description (empty string clears).")
|
|
198
|
+
@click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
|
|
199
|
+
def label_update(
|
|
200
|
+
label_uuid: str,
|
|
201
|
+
name: str | None,
|
|
202
|
+
color: str | None,
|
|
203
|
+
description: str | None,
|
|
204
|
+
json_mode: bool,
|
|
205
|
+
) -> None:
|
|
206
|
+
"""Update an organization Label."""
|
|
207
|
+
enforce_plan_access("label_update", json_mode=json_mode)
|
|
208
|
+
body: dict[str, Any] = {}
|
|
209
|
+
if name is not None:
|
|
210
|
+
body["name"] = name
|
|
211
|
+
if color is not None:
|
|
212
|
+
body["color"] = color
|
|
213
|
+
if description is not None:
|
|
214
|
+
body["description"] = description
|
|
215
|
+
if not body:
|
|
216
|
+
raise click.UsageError("Pass at least one of --name, --color, or --description.")
|
|
217
|
+
|
|
218
|
+
client = require_auth()
|
|
219
|
+
try:
|
|
220
|
+
with console.status("Updating label..."):
|
|
221
|
+
data: dict[str, Any] = client.update_label(label_uuid, body)
|
|
222
|
+
except APIError as exc:
|
|
223
|
+
exit_for_api_error(exc, json_mode)
|
|
224
|
+
|
|
225
|
+
if json_mode:
|
|
226
|
+
emit_json(data)
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
print_success(f"Updated label '{data.get('name', label_uuid)}'.")
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@label.command("archive")
|
|
233
|
+
@click.argument("label_uuid")
|
|
234
|
+
@click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
|
|
235
|
+
def label_archive(label_uuid: str, json_mode: bool) -> None:
|
|
236
|
+
"""Archive a Label (idempotent)."""
|
|
237
|
+
enforce_plan_access("label_archive", json_mode=json_mode)
|
|
238
|
+
client = require_auth()
|
|
239
|
+
try:
|
|
240
|
+
with console.status("Archiving label..."):
|
|
241
|
+
data: dict[str, Any] = client.archive_label(label_uuid)
|
|
242
|
+
except APIError as exc:
|
|
243
|
+
exit_for_api_error(exc, json_mode)
|
|
244
|
+
|
|
245
|
+
if json_mode:
|
|
246
|
+
emit_json(data)
|
|
247
|
+
return
|
|
248
|
+
|
|
249
|
+
print_success(f"Archived label '{data.get('name', label_uuid)}'.")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@label.command("delete")
|
|
253
|
+
@click.argument("label_uuid")
|
|
254
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt.")
|
|
255
|
+
@click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
|
|
256
|
+
def label_delete(label_uuid: str, yes: bool, json_mode: bool) -> None:
|
|
257
|
+
"""Hard-delete a Label (elevated only; fails when in use)."""
|
|
258
|
+
enforce_plan_access("label_delete", json_mode=json_mode)
|
|
259
|
+
client: DailyBotClient = require_auth()
|
|
260
|
+
|
|
261
|
+
if (
|
|
262
|
+
not yes
|
|
263
|
+
and not json_mode
|
|
264
|
+
and not click.confirm(
|
|
265
|
+
f"Permanently delete label {label_uuid}? This cannot be undone.",
|
|
266
|
+
default=False,
|
|
267
|
+
)
|
|
268
|
+
):
|
|
269
|
+
raise SystemExit(0)
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
with console.status("Deleting label..."):
|
|
273
|
+
client.delete_label(label_uuid)
|
|
274
|
+
except APIError as exc:
|
|
275
|
+
exit_for_api_error(exc, json_mode)
|
|
276
|
+
|
|
277
|
+
if json_mode:
|
|
278
|
+
emit_json({"deleted": True, "uuid": label_uuid})
|
|
279
|
+
return
|
|
280
|
+
|
|
281
|
+
print_success(f"Deleted label {label_uuid}.")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@label.command("assign")
|
|
285
|
+
@click.argument("entity_uuid")
|
|
286
|
+
@click.option(
|
|
287
|
+
"--type",
|
|
288
|
+
"entity_type",
|
|
289
|
+
required=True,
|
|
290
|
+
callback=_parse_label_entity_type,
|
|
291
|
+
help="Entity type: forms, checkins, or workflows (automations alias).",
|
|
292
|
+
)
|
|
293
|
+
@click.option(
|
|
294
|
+
"--label",
|
|
295
|
+
"label_refs",
|
|
296
|
+
multiple=True,
|
|
297
|
+
help="Label UUID to attach (repeatable, or comma-separated). Replace-set.",
|
|
298
|
+
)
|
|
299
|
+
@click.option(
|
|
300
|
+
"--clear",
|
|
301
|
+
is_flag=True,
|
|
302
|
+
help="Remove every Label from this entity (replace with an empty set).",
|
|
303
|
+
)
|
|
304
|
+
@click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
|
|
305
|
+
def label_assign(
|
|
306
|
+
entity_uuid: str,
|
|
307
|
+
entity_type: str,
|
|
308
|
+
label_refs: tuple[str, ...],
|
|
309
|
+
clear: bool,
|
|
310
|
+
json_mode: bool,
|
|
311
|
+
) -> None:
|
|
312
|
+
"""Replace the Labels on one form, check-in, or workflow (web picker parity).
|
|
313
|
+
|
|
314
|
+
\b
|
|
315
|
+
Same as the chip picker in the Dailybot web app: the list you pass becomes
|
|
316
|
+
the full set. Use --clear to detach every Label. Add or remove on many
|
|
317
|
+
items at once with `label batch`.
|
|
318
|
+
"""
|
|
319
|
+
label_uuids: list[str] = _parse_label_uuid_list(label_refs)
|
|
320
|
+
if clear and label_uuids:
|
|
321
|
+
raise click.UsageError("Pass either --label or --clear, not both.")
|
|
322
|
+
if not clear and not label_uuids:
|
|
323
|
+
raise click.UsageError("Pass at least one --label, or --clear to detach all.")
|
|
324
|
+
|
|
325
|
+
client = require_auth()
|
|
326
|
+
try:
|
|
327
|
+
with console.status("Updating labels..."):
|
|
328
|
+
data: dict[str, Any] = client.assign_entity_labels(
|
|
329
|
+
entity_type,
|
|
330
|
+
entity_uuid,
|
|
331
|
+
label_uuids,
|
|
332
|
+
)
|
|
333
|
+
except APIError as exc:
|
|
334
|
+
exit_for_api_error(exc, json_mode)
|
|
335
|
+
|
|
336
|
+
if json_mode:
|
|
337
|
+
emit_json(data)
|
|
338
|
+
return
|
|
339
|
+
|
|
340
|
+
attached: list[dict[str, Any]] = data.get("labels") or []
|
|
341
|
+
print_success(f"Updated labels on {entity_type} {data.get('uuid', entity_uuid)}.")
|
|
342
|
+
print_label_assignment(str(data.get("uuid", entity_uuid)), attached)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@label.command("batch")
|
|
346
|
+
@click.option(
|
|
347
|
+
"--type",
|
|
348
|
+
"entity_type",
|
|
349
|
+
required=True,
|
|
350
|
+
callback=_parse_label_entity_type,
|
|
351
|
+
help="Entity type: forms, checkins, or workflows (automations alias).",
|
|
352
|
+
)
|
|
353
|
+
@click.option(
|
|
354
|
+
"--uuids",
|
|
355
|
+
required=True,
|
|
356
|
+
help="Comma-separated form, check-in, or workflow UUIDs.",
|
|
357
|
+
)
|
|
358
|
+
@click.option(
|
|
359
|
+
"--label",
|
|
360
|
+
"label_refs",
|
|
361
|
+
multiple=True,
|
|
362
|
+
help="Label UUID (repeatable, or comma-separated).",
|
|
363
|
+
)
|
|
364
|
+
@click.option(
|
|
365
|
+
"--mode",
|
|
366
|
+
type=click.Choice(_BATCH_MODES, case_sensitive=False),
|
|
367
|
+
default="add",
|
|
368
|
+
show_default=True,
|
|
369
|
+
help="add / remove / replace Labels on every listed entity.",
|
|
370
|
+
)
|
|
371
|
+
@click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
|
|
372
|
+
def label_batch(
|
|
373
|
+
entity_type: str,
|
|
374
|
+
uuids: str,
|
|
375
|
+
label_refs: tuple[str, ...],
|
|
376
|
+
mode: str,
|
|
377
|
+
json_mode: bool,
|
|
378
|
+
) -> None:
|
|
379
|
+
"""Add, remove, or replace Labels on many forms, check-ins, or workflows."""
|
|
380
|
+
entity_uuids: list[str] = [part.strip() for part in uuids.split(",") if part.strip()]
|
|
381
|
+
if not entity_uuids:
|
|
382
|
+
raise click.UsageError("--uuids must contain at least one UUID.")
|
|
383
|
+
label_uuids: list[str] = _parse_label_uuid_list(label_refs)
|
|
384
|
+
if not label_uuids:
|
|
385
|
+
raise click.UsageError("Pass at least one --label.")
|
|
386
|
+
|
|
387
|
+
client = require_auth()
|
|
388
|
+
try:
|
|
389
|
+
with console.status("Updating labels..."):
|
|
390
|
+
data: dict[str, Any] = client.batch_entity_labels(
|
|
391
|
+
entity_type=entity_type,
|
|
392
|
+
entity_uuids=entity_uuids,
|
|
393
|
+
label_uuids=label_uuids,
|
|
394
|
+
mode=mode.lower(),
|
|
395
|
+
)
|
|
396
|
+
except APIError as exc:
|
|
397
|
+
exit_for_api_error(exc, json_mode)
|
|
398
|
+
|
|
399
|
+
if json_mode:
|
|
400
|
+
emit_json(data)
|
|
401
|
+
return
|
|
402
|
+
|
|
403
|
+
updated: int = int(data.get("updated_count") or len(entity_uuids))
|
|
404
|
+
print_success(f"{mode.lower()} {len(label_uuids)} label(s) on {updated} {entity_type} item(s).")
|
|
@@ -115,6 +115,12 @@ ERROR_CODE_MESSAGES: dict[str, str] = {
|
|
|
115
115
|
"Your role doesn't allow authoring check-ins. Ask an admin or manager. "
|
|
116
116
|
"The CLI acts within your role and can't elevate."
|
|
117
117
|
),
|
|
118
|
+
"paid_plan_required": "Organization Labels require a paid plan with Feature.LABELS enabled.",
|
|
119
|
+
"feature_not_available": "Organization Labels are not available on this plan.",
|
|
120
|
+
"guest_not_allowed": "Guests cannot manage organization Labels.",
|
|
121
|
+
"duplicate_name": "A label with this name already exists.",
|
|
122
|
+
"archived_label_not_assignable": "This label is archived and cannot be assigned.",
|
|
123
|
+
"permission_denied": "You don't have permission for this Labels action.",
|
|
118
124
|
"form_edit_forbidden": (
|
|
119
125
|
"You don't have permission to edit this form (you're not the owner or an admin). "
|
|
120
126
|
"The CLI acts within your role and can't elevate."
|
|
@@ -514,6 +520,23 @@ def normalize_checkin_list_json(data: dict[str, Any]) -> dict[str, Any]:
|
|
|
514
520
|
return {"pending_checkins": pending, "count": data.get("count", len(pending))}
|
|
515
521
|
|
|
516
522
|
|
|
523
|
+
def normalize_checkin_entity_json(checkin: dict[str, Any]) -> dict[str, Any]:
|
|
524
|
+
"""Ensure authoring check-in payloads expose a stable ``uuid`` field.
|
|
525
|
+
|
|
526
|
+
Create/config responses from ``/v1/checkins/`` historically return the
|
|
527
|
+
follow-up id under ``id`` (not ``uuid``). Forms and Labels use ``uuid``
|
|
528
|
+
everywhere, so scripting ``checkin create --json`` → ``label assign``
|
|
529
|
+
was awkward. Copy ``id`` into ``uuid`` when missing; never overwrite an
|
|
530
|
+
explicit ``uuid``.
|
|
531
|
+
"""
|
|
532
|
+
enriched: dict[str, Any] = dict(checkin)
|
|
533
|
+
if not enriched.get("uuid"):
|
|
534
|
+
legacy_id: Any = enriched.get("id") or enriched.get("followup_uuid")
|
|
535
|
+
if legacy_id:
|
|
536
|
+
enriched["uuid"] = str(legacy_id)
|
|
537
|
+
return enriched
|
|
538
|
+
|
|
539
|
+
|
|
517
540
|
def find_pending_checkin(
|
|
518
541
|
pending_checkins: list[dict[str, Any]],
|
|
519
542
|
followup_uuid: str,
|
|
@@ -16,6 +16,7 @@ from dailybot_cli.commands.public_api_helpers import (
|
|
|
16
16
|
exit_for_api_error,
|
|
17
17
|
find_pending_checkin,
|
|
18
18
|
get_current_user_uuid,
|
|
19
|
+
normalize_checkin_entity_json,
|
|
19
20
|
normalize_checkin_list_json,
|
|
20
21
|
parse_answer_flags,
|
|
21
22
|
)
|
|
@@ -671,7 +672,7 @@ def execute_checkin_show(
|
|
|
671
672
|
exit_for_api_error(exc, json_mode)
|
|
672
673
|
|
|
673
674
|
if json_mode:
|
|
674
|
-
emit_json(detail)
|
|
675
|
+
emit_json(normalize_checkin_entity_json(detail))
|
|
675
676
|
return
|
|
676
677
|
print_checkin_detail(detail)
|
|
677
678
|
|
|
@@ -170,6 +170,49 @@ def print_kudos_wall_of_fame(data: dict[str, Any]) -> None:
|
|
|
170
170
|
)
|
|
171
171
|
|
|
172
172
|
|
|
173
|
+
def print_label_assignment(entity_uuid: str, labels: list[dict[str, Any]]) -> None:
|
|
174
|
+
"""Render the Labels currently attached to one form, check-in, or workflow."""
|
|
175
|
+
names: list[str] = [str(item.get("name") or item.get("uuid") or "—") for item in labels]
|
|
176
|
+
attached: str = ", ".join(names) if names else "(none)"
|
|
177
|
+
console.print(f"[bold]{entity_uuid}[/bold] → {attached}")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def print_labels_table(labels: list[dict[str, Any]]) -> None:
|
|
181
|
+
"""Render a compact table of organization Labels."""
|
|
182
|
+
if not labels:
|
|
183
|
+
console.print("[dim]No labels found.[/dim]")
|
|
184
|
+
return
|
|
185
|
+
table: Table = Table(title="Labels")
|
|
186
|
+
table.add_column("Name", style="cyan")
|
|
187
|
+
table.add_column("UUID", style="dim", no_wrap=True)
|
|
188
|
+
table.add_column("Color")
|
|
189
|
+
table.add_column("Usage", justify="right")
|
|
190
|
+
table.add_column("Archived", justify="center")
|
|
191
|
+
for label in labels:
|
|
192
|
+
usage: dict[str, Any] = label.get("usage") or {}
|
|
193
|
+
archived: str = "[green]yes[/green]" if label.get("is_archived") else "[dim]no[/dim]"
|
|
194
|
+
table.add_row(
|
|
195
|
+
str(label.get("name", "—")),
|
|
196
|
+
str(label.get("uuid", "—")),
|
|
197
|
+
str(label.get("color", "—")),
|
|
198
|
+
str(usage.get("total", 0)),
|
|
199
|
+
archived,
|
|
200
|
+
)
|
|
201
|
+
console.print(table)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def print_featured_summary(data: dict[str, Any]) -> None:
|
|
205
|
+
"""Render Featured UUID list for one entity type."""
|
|
206
|
+
entity_type: str = str(data.get("entity_type", "—"))
|
|
207
|
+
uuids: list[str] = [str(value) for value in data.get("entity_uuids", [])]
|
|
208
|
+
console.print(f"[bold]Featured {entity_type}[/bold] ({len(uuids)})")
|
|
209
|
+
if not uuids:
|
|
210
|
+
console.print("[dim]None starred yet.[/dim]")
|
|
211
|
+
return
|
|
212
|
+
for uuid in uuids:
|
|
213
|
+
console.print(f" • {uuid}")
|
|
214
|
+
|
|
215
|
+
|
|
173
216
|
def print_workflows_table(workflows: list[dict[str, Any]]) -> None:
|
|
174
217
|
"""Render a compact table of workflows (name, trigger, active, runs)."""
|
|
175
218
|
if not workflows:
|
|
@@ -1419,8 +1462,10 @@ def print_form_created(form: dict[str, Any], *, updated: bool = False) -> None:
|
|
|
1419
1462
|
def print_checkin_created(checkin: dict[str, Any], *, updated: bool = False) -> None:
|
|
1420
1463
|
"""Display a created (or, with ``updated=True``, edited) check-in + summary."""
|
|
1421
1464
|
name: str = str(checkin.get("name") or "")
|
|
1422
|
-
|
|
1423
|
-
|
|
1465
|
+
checkin_uuid: str = str(
|
|
1466
|
+
checkin.get("uuid") or checkin.get("id") or checkin.get("followup_uuid") or ""
|
|
1467
|
+
)
|
|
1468
|
+
lines: list[str] = [f"[bold]{name}[/bold]", f"UUID: {checkin_uuid}"]
|
|
1424
1469
|
schedule: dict[str, Any] = checkin.get("schedule") or {}
|
|
1425
1470
|
if schedule:
|
|
1426
1471
|
days: Any = schedule.get("days")
|
|
@@ -14,12 +14,14 @@ from dailybot_cli.commands.checkin import checkin
|
|
|
14
14
|
from dailybot_cli.commands.config import config
|
|
15
15
|
from dailybot_cli.commands.conversation import conversation
|
|
16
16
|
from dailybot_cli.commands.env import env
|
|
17
|
+
from dailybot_cli.commands.featured import featured
|
|
17
18
|
from dailybot_cli.commands.form import form
|
|
18
19
|
from dailybot_cli.commands.hook import hook
|
|
19
20
|
from dailybot_cli.commands.identity import me, org
|
|
20
21
|
from dailybot_cli.commands.interactive import run_interactive
|
|
21
22
|
from dailybot_cli.commands.interactive_chat import interactive
|
|
22
23
|
from dailybot_cli.commands.kudos import kudos
|
|
24
|
+
from dailybot_cli.commands.label import label
|
|
23
25
|
from dailybot_cli.commands.status import status
|
|
24
26
|
from dailybot_cli.commands.team import team
|
|
25
27
|
from dailybot_cli.commands.uninstall import uninstall
|
|
@@ -121,6 +123,8 @@ cli.add_command(user)
|
|
|
121
123
|
cli.add_command(me)
|
|
122
124
|
cli.add_command(org)
|
|
123
125
|
cli.add_command(workflow)
|
|
126
|
+
cli.add_command(label)
|
|
127
|
+
cli.add_command(featured)
|
|
124
128
|
cli.add_command(agent)
|
|
125
129
|
cli.add_command(chat)
|
|
126
130
|
cli.add_command(conversation)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dailybot-cli
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.9.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
|
|
@@ -25,12 +25,14 @@ dailybot_cli/commands/checkin.py
|
|
|
25
25
|
dailybot_cli/commands/config.py
|
|
26
26
|
dailybot_cli/commands/conversation.py
|
|
27
27
|
dailybot_cli/commands/env.py
|
|
28
|
+
dailybot_cli/commands/featured.py
|
|
28
29
|
dailybot_cli/commands/form.py
|
|
29
30
|
dailybot_cli/commands/hook.py
|
|
30
31
|
dailybot_cli/commands/identity.py
|
|
31
32
|
dailybot_cli/commands/interactive.py
|
|
32
33
|
dailybot_cli/commands/interactive_chat.py
|
|
33
34
|
dailybot_cli/commands/kudos.py
|
|
35
|
+
dailybot_cli/commands/label.py
|
|
34
36
|
dailybot_cli/commands/public_api_helpers.py
|
|
35
37
|
dailybot_cli/commands/query_options.py
|
|
36
38
|
dailybot_cli/commands/status.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dailybot-cli"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.9.0"
|
|
8
8
|
description = "Dailybot CLI - The command-line bridge between humans and agents. Progress reports, observability, and workflow automation for modern teams across Slack, Google Chat, Discord, MS Teams, and more."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|