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.
Files changed (53) hide show
  1. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/PKG-INFO +1 -1
  2. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/api_client.py +209 -0
  3. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/checkin.py +3 -2
  4. dailybot_cli-3.9.0/dailybot_cli/commands/featured.py +147 -0
  5. dailybot_cli-3.9.0/dailybot_cli/commands/label.py +404 -0
  6. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/public_api_helpers.py +23 -0
  7. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/user_scoped_actions.py +2 -1
  8. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/display.py +47 -2
  9. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/main.py +4 -0
  10. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli.egg-info/PKG-INFO +1 -1
  11. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli.egg-info/SOURCES.txt +2 -0
  12. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/pyproject.toml +1 -1
  13. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/LICENSE +0 -0
  14. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/README.md +0 -0
  15. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/__init__.py +0 -0
  16. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/__init__.py +0 -0
  17. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/agent.py +0 -0
  18. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/ask.py +0 -0
  19. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/auth.py +0 -0
  20. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/authoring_helpers.py +0 -0
  21. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/channels.py +0 -0
  22. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/chat.py +0 -0
  23. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/config.py +0 -0
  24. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/conversation.py +0 -0
  25. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/env.py +0 -0
  26. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/form.py +0 -0
  27. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/hook.py +0 -0
  28. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/identity.py +0 -0
  29. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/interactive.py +0 -0
  30. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/interactive_chat.py +0 -0
  31. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/kudos.py +0 -0
  32. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/query_options.py +0 -0
  33. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/status.py +0 -0
  34. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/team.py +0 -0
  35. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/uninstall.py +0 -0
  36. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/update.py +0 -0
  37. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/upgrade.py +0 -0
  38. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/user.py +0 -0
  39. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/version.py +0 -0
  40. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/commands/workflow.py +0 -0
  41. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/config.py +0 -0
  42. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/install_method.py +0 -0
  43. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/ledger.py +0 -0
  44. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/tui/__init__.py +0 -0
  45. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/tui/app.py +0 -0
  46. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/tui/commands.py +0 -0
  47. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/tui/conversation.py +0 -0
  48. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli/tui/intents.py +0 -0
  49. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli.egg-info/dependency_links.txt +0 -0
  50. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli.egg-info/entry_points.txt +0 -0
  51. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli.egg-info/requires.txt +0 -0
  52. {dailybot_cli-3.8.0 → dailybot_cli-3.9.0}/dailybot_cli.egg-info/top_level.txt +0 -0
  53. {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.8.0
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
- checkin_id: str = str(checkin.get("id") or checkin.get("uuid") or "")
1423
- lines: list[str] = [f"[bold]{name}[/bold]", f"ID: {checkin_id}"]
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.8.0
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.8.0"
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