Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .agents/skills/dailybot/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ machine — permissions, consent guarantees, and a self-audit you can run — is

## What it does

Thirteen coordinated capabilities, with smart routing between them:
Fourteen coordinated capabilities, with smart routing between them:

| Capability | Sub-skill | When it fires |
|------------|-----------|---------------|
Expand All @@ -67,6 +67,7 @@ Thirteen coordinated capabilities, with smart routing between them:
| **Teams** | `dailybot-teams` | List teams, inspect members, resolve a team name → UUID (used as a resolver by other skills) — **plus account context**: `dailybot me` (who am I / role), `dailybot org` (which org), and `dailybot user get` (one user's profile) |
| **Forms** | `dailybot-forms` | List, submit, update, or transition forms — including workflow-state forms with audience permissions (`form list` is now **org-scoped** by default, with `--mine` to narrow to your own; list + responses support pagination / search / date filters) — **plus authoring**: create/configure a form (workflow states, permissions, anonymous/public/approval, ChatOps command) and manage its questions |
| **Workflows** | `dailybot-workflow` | Developer wants to **read or trigger** the org's workflows — `workflow list` (paginated/searchable, with `--filter api_trigger`), `workflow get`, and `workflow trigger` (fire an API-triggerable workflow with an optional JSON payload). Creating/editing workflows is web-app only. Plan-gated |
| **Labels** | `dailybot-labels` | Create org Labels and assign them to forms, check-ins, and workflows (`label assign` / `label batch`) — same chip picker as the web app |
| **Report channels** | `dailybot-channels` | Discover report-channel UUIDs to attach to forms/check-ins with `--report-channel` |
| **Per-repo API keys** | `dailybot-env` | Configure `.dailybot/env.json` — an **opt-in, gitignored** file that carries API keys + URLs for one or more environments (live, local, staging) so the developer can be "logged into different orgs in different repos". `dailybot env add / use / show / list / remove / off / on`. Pack baseline (`>= 3.8.0`) |

Expand Down Expand Up @@ -242,6 +243,7 @@ the full step-by-step workflow.
| "list / search / browse my forms (or kudos, or workflows) with pagination", "only the first N", "since last week", "grep for retro" | The matching sub-skill — all share [`shared/list-query-and-errors.md`](shared/list-query-and-errors.md) for the query flags |
| "who am I?", "what's my role?", "which org am I in?", "show a user's profile" | **Teams** → read [`teams/SKILL.md`](teams/SKILL.md) § Step 4.5 (`me` / `org` / `user get`) |
| "browse kudos", "kudos I received / gave", "org kudos stats", "who's on the wall of fame?" | **Kudos** → read [`kudos/SKILL.md`](kudos/SKILL.md) § Browsing kudos |
| "add a label to a form / check-in / workflow", "tag this standup with Sprint", "assign organization labels" | **Labels** → read [`labels/SKILL.md`](labels/SKILL.md) |
| "list my workflows", "show workflows", "what's in workflow X?" | **Workflows** → read [`workflow/SKILL.md`](workflow/SKILL.md) |
| "trigger the deploy workflow", "fire automation X", "run workflow `<uuid>`", "trigger workflow with payload" | **Workflows** → read [`workflow/SKILL.md`](workflow/SKILL.md) § Step 4 (Trigger) |
| "which channels can Dailybot post to?", "list report channels", "I need a channel UUID for the form / check-in" | **Channels** → read [`channels/SKILL.md`](channels/SKILL.md) |
Expand Down
69 changes: 69 additions & 0 deletions .agents/skills/dailybot/labels/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
name: dailybot-labels
description: Create organization Labels and attach them to forms, check-ins, and workflows (automations) via the Dailybot CLI — same taxonomy as the web settings page and row chip picker. Use when the developer wants to add, replace, or clear Labels on those entities, or to create/list Labels.
---

# Dailybot Labels

Organization Labels are a **shared taxonomy** (not private Featured stars). The
web picker on a form / check-in / automation row is a **replace-set**. Match
that with `dailybot label assign`. Bulk add/remove uses `dailybot label batch`.

Requires CLI from the personalized-labels branch (commands `label assign` and
`label batch`). Confirm with `dailybot label assign --help`.

## Auth

Same session as the rest of the CLI (`dailybot login` or `DAILYBOT_API_KEY`).
Point at staging with `--api-url https://staging-api.dailybot.com` when testing
there. Server entitlement (`dailybot label entitlement`) is the source of
truth — do not assume a paid plan is required.

## Create and list Labels

```bash
dailybot label list
dailybot label create --name "Sprint" --color "#4A90E2"
```

Copy UUIDs from `label list` (or `--json`).

## Attach to one entity (web picker parity)

```bash
# Forms
dailybot label assign <form-uuid> --type forms --label <label-uuid>

# Check-ins
dailybot label assign <checkin-uuid> --type checkins --label <label-uuid>

# Workflows / Automations (same API; --type automations is an alias)
dailybot label assign <workflow-uuid> --type workflows --label <label-uuid>
```

Repeat `--label` (or comma-separate) for several Labels. The list **replaces**
whatever was on the entity.

```bash
dailybot label assign <form-uuid> --type forms --clear
```

## Bulk add / remove

```bash
dailybot label batch --type forms --uuids <uuid1>,<uuid2> --label <label-uuid> --mode add
dailybot label batch --type checkins --uuids <uuid> --label <label-uuid> --mode remove
dailybot label batch --type workflows --uuids <uuid> --label <label-uuid> --mode replace
```

`--mode` is `add` (default), `remove`, or `replace`.

## After authoring

`dailybot form create` / `checkin create` do **not** take `--labels`. Create
first, then `label assign` with the new UUID from `--json`.

## Not this skill

Private stars → `dailybot featured`. Workflow *states* named "labels" on a
form response → `dailybot-forms` transitions.
209 changes: 209 additions & 0 deletions dailybot_cli/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,37 @@ def _merge_list_query(
return params


def _merge_dashboard_enrichment_query(
params: dict[str, Any],
*,
labels: list[str] | None = None,
featured: bool | None = None,
prioritize_featured: bool | None = None,
) -> dict[str, Any]:
"""Merge Labels / Featured dashboard enrichment query params."""
if labels:
params["labels"] = ",".join(labels)
if featured is not None:
params["featured"] = "true" if featured else "false"
if prioritize_featured is not None:
params["prioritize_featured"] = "true" if prioritize_featured else "false"
return params


def _label_entity_collection(entity_type: str) -> str:
"""Map CLI entity type (including web 'automations') to the public API path."""
normalized: str = entity_type.strip().lower()
if normalized in {"automations", "workflows", "workflow"}:
return "workflows"
if normalized in {"forms", "form"}:
return "forms"
if normalized in {"checkins", "checkin", "check-ins"}:
return "checkins"
raise ValueError(
f"entity type must be one of: forms, checkins, workflows (got {entity_type!r})"
)


def _fill_meta(meta: dict[str, Any] | None, result: "PaginatedResult") -> None:
"""Populate a caller-provided meta dict with pagination totals, if given."""
if meta is not None:
Expand Down Expand Up @@ -640,6 +671,9 @@ def list_checkins(
fetch_all: bool = True,
limit: int | None = None,
meta: dict[str, Any] | None = None,
labels: list[str] | None = None,
featured: bool | None = None,
prioritize_featured: bool | None = None,
) -> list[dict[str, Any]]:
"""GET /v1/checkins/ — fetch visible check-ins with optional search/paging."""
params: dict[str, Any] = {}
Expand All @@ -652,6 +686,12 @@ def list_checkins(
if include_archived:
params["include_archived"] = "true"
_merge_list_query(params, search=search, start_date=start_date, end_date=end_date)
_merge_dashboard_enrichment_query(
params,
labels=labels,
featured=featured,
prioritize_featured=prioritize_featured,
)
result: PaginatedResult = self._paginated_get(
f"{self.api_url}/v1/checkins/",
params=params,
Expand Down Expand Up @@ -963,6 +1003,9 @@ def list_forms(
fetch_all: bool = True,
limit: int | None = None,
meta: dict[str, Any] | None = None,
labels: list[str] | None = None,
featured: bool | None = None,
prioritize_featured: bool | None = None,
) -> list[dict[str, Any]]:
"""GET /v1/forms/ — optionally expand questions, search, and page.

Expand Down Expand Up @@ -990,6 +1033,12 @@ def list_forms(
if is_ascend:
params["is_ascend"] = "true"
_merge_list_query(params, search=search, start_date=start_date, end_date=end_date)
_merge_dashboard_enrichment_query(
params,
labels=labels,
featured=featured,
prioritize_featured=prioritize_featured,
)
result: PaginatedResult = self._paginated_get(
f"{self.api_url}/v1/forms/",
params=params,
Expand Down Expand Up @@ -1442,10 +1491,19 @@ def list_workflows(
fetch_all: bool = True,
limit: int | None = None,
meta: dict[str, Any] | None = None,
labels: list[str] | None = None,
featured: bool | None = None,
prioritize_featured: bool | None = None,
) -> list[dict[str, Any]]:
"""GET /v1/workflows/ — list workflows (plan-gated feature)."""
params: dict[str, Any] = {}
_merge_list_query(params, search=search, start_date=start_date, end_date=end_date)
_merge_dashboard_enrichment_query(
params,
labels=labels,
featured=featured,
prioritize_featured=prioritize_featured,
)
result: PaginatedResult = self._paginated_get(
f"{self.api_url}/v1/workflows/",
params=params,
Expand Down Expand Up @@ -1789,6 +1847,157 @@ def mark_agent_messages_read(
)
return self._handle_response(response)

# --- Organization Labels (/v1/labels/) ---

def get_labels_entitlement(self) -> dict[str, Any]:
"""GET /v1/labels/entitlement/ — org Labels feature flags for the caller."""
response: httpx.Response = self._request("GET", f"{self.api_url}/v1/labels/entitlement/")
return self._handle_response(response)

def list_labels(
self,
*,
search: str | None = None,
is_archived: bool = False,
limit: int = 20,
offset: int = 0,
) -> dict[str, Any]:
"""GET /v1/labels/ — paginated org Labels (limit/offset)."""
params: dict[str, Any] = {
"limit": max(1, min(limit, 100)),
"offset": max(0, offset),
"is_archived": is_archived,
}
if search:
params["search"] = search
response: httpx.Response = self._request("GET", f"{self.api_url}/v1/labels/", params=params)
return self._handle_response(response)

def get_label(self, label_uuid: str) -> dict[str, Any]:
"""GET /v1/labels/<uuid>/ — one organization Label."""
response: httpx.Response = self._request("GET", f"{self.api_url}/v1/labels/{label_uuid}/")
return self._handle_response(response)

def create_label(
self,
*,
name: str,
color: str | None = None,
description: str | None = None,
) -> dict[str, Any]:
"""POST /v1/labels/ — create an organization Label."""
body: dict[str, Any] = {"name": name}
if color is not None:
body["color"] = color
if description is not None:
body["description"] = description
response: httpx.Response = self._request("POST", f"{self.api_url}/v1/labels/", json=body)
return self._handle_response(response)

def update_label(self, label_uuid: str, body: dict[str, Any]) -> dict[str, Any]:
"""PATCH /v1/labels/<uuid>/ — update an organization Label."""
response: httpx.Response = self._request(
"PATCH", f"{self.api_url}/v1/labels/{label_uuid}/", json=body
)
return self._handle_response(response)

def delete_label(self, label_uuid: str) -> None:
"""DELETE /v1/labels/<uuid>/ — hard-delete (elevated only)."""
response: httpx.Response = self._request(
"DELETE", f"{self.api_url}/v1/labels/{label_uuid}/"
)
if response.status_code == 204:
return
self._handle_response(response)

def archive_label(self, label_uuid: str) -> dict[str, Any]:
"""POST /v1/labels/<uuid>/archive/ — archive a Label."""
response: httpx.Response = self._request(
"POST", f"{self.api_url}/v1/labels/{label_uuid}/archive/"
)
return self._handle_response(response)

def assign_entity_labels(
self,
entity_type: str,
entity_uuid: str,
label_uuids: list[str],
) -> dict[str, Any]:
"""POST /v1/{forms|checkins|workflows}/{uuid}/labels/ — replace-set Labels."""
collection: str = _label_entity_collection(entity_type)
response: httpx.Response = self._request(
"POST",
f"{self.api_url}/v1/{collection}/{entity_uuid}/labels/",
json={"label_uuids": label_uuids},
)
return self._handle_response(response)

def batch_entity_labels(
self,
*,
entity_type: str,
entity_uuids: list[str],
label_uuids: list[str],
mode: str,
) -> dict[str, Any]:
"""POST /v1/{forms|checkins|workflows}/labels/batch/ — add/remove/replace."""
collection: str = _label_entity_collection(entity_type)
response: httpx.Response = self._request(
"POST",
f"{self.api_url}/v1/{collection}/labels/batch/",
json={
"entity_uuids": entity_uuids,
"label_uuids": label_uuids,
"mode": mode,
},
)
return self._handle_response(response)

# --- Private Featured stars (/v1/me/featured/) ---

def list_featured(self, *, entity_type: str) -> dict[str, Any]:
"""GET /v1/me/featured/?entity_type= — list Featured entity UUIDs for the caller."""
response: httpx.Response = self._request(
"GET",
f"{self.api_url}/v1/me/featured/",
params={"entity_type": entity_type},
)
return self._handle_response(response)

def set_featured(
self,
entity_type: str,
entity_uuid: str,
*,
featured: bool,
) -> dict[str, Any]:
"""PUT /v1/me/featured/{entity_type}/{uuid}/ — toggle Featured for one entity."""
response: httpx.Response = self._request(
"PUT",
f"{self.api_url}/v1/me/featured/{entity_type}/{entity_uuid}/",
json={"featured": featured},
)
return self._handle_response(response)

def batch_featured(
self,
*,
entity_type: str,
entity_uuids: list[str],
featured: bool,
) -> dict[str, Any]:
"""POST /v1/me/featured/batch/ — batch feature/unfeature entities."""
response: httpx.Response = self._request(
"POST",
f"{self.api_url}/v1/me/featured/batch/",
json={
"entity_type": entity_type,
"entity_uuids": entity_uuids,
"featured": featured,
},
)
return self._handle_response(response)

# --- Agent registration endpoints ---

def get_registration_challenge(self) -> dict[str, Any]:
Expand Down
Loading