Skip to content
Merged
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
38 changes: 32 additions & 6 deletions .agents/skills/dailybot/forms/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ The resolver step (4) is the customer-extension hook. See **Step 7 — Custom-sk
dailybot form list --json
```

Returns all forms visible to the logged-in user. **As of `dailybot-cli >= 3.2.0` this is org-scoped**: it lists every form the caller can see on the webapp list view — an org **admin sees all** the org's forms; a member sees forms flagged for the list view plus their own. (Before 3.2.0 the endpoint returned only the caller's *own* forms even for admins — a server-side bug; if a developer reports "I only see a handful of my forms," check `dailybot --version` and have them `dailybot upgrade`.) The shape is stable and machine-readable:
Returns all forms in the caller's organization. Every org member sees every org form; capabilities (editing, response visibility, state changes) are governed by each form's permissions. Pass `--mine` to narrow to only your own forms. (If a developer reports "I only see a handful of my forms," check `dailybot --version` and have them `dailybot upgrade` — older CLIs had a server-side visibility bug.) The shape is stable and machine-readable:

```json
[
Expand Down Expand Up @@ -161,22 +161,48 @@ dailybot form list --all --json
dailybot form list --page 2 --page-size 20 --json
```

### Scope the list to your own forms — `--mine` (CLI >= 3.2.0)
### Filter by owner — `--mine` and `--owner` (CLI >= 3.5.2)

Since the default is now org-scoped, pass **`--mine`** to narrow the result to
only the forms **you own** (it sends `owner=me` to the API). Useful when an admin
wants their personal forms out of the full org list.
Pass **`--mine`** to narrow the result to only the forms **you own**, or
**`--owner`** (repeatable) to filter by specific owners. `--owner` accepts
a UUID, email, or name — non-UUID values are resolved via the org directory.
The two flags can be combined (AND semantics with every other filter).

```bash
# Only the forms I own:
dailybot form list --mine --json

# Forms owned by a specific person (by name):
dailybot form list --owner "Jane Doe" --json

# Forms owned by two people (by UUID):
dailybot form list --owner <uuid-1> --owner <uuid-2> --json
```

> **Deprecation:** the `--filter me` scope is deprecated server-side. Use
> `--mine` or `--owner` instead — `--filter me` still works but maps to
> the legacy `filter=me` parameter.

### Form owners picker — `form owners` (CLI >= 3.5.2)

A lightweight endpoint to discover which org members own at least one form,
without pulling the full member directory.

```bash
dailybot form owners # table of owners
dailybot form owners --search jane # search by name/email
dailybot form owners --json # machine-readable
```

Each result has `uuid`, `full_name`, `image`, `role`, and optionally `email`
(email is only visible to admins/managers — the CLI must not assume it exists).

### Server-side filtering, sorting, and archived forms (CLI >= 3.5.0)

| Flag | Values | Description |
|------|--------|-------------|
| `--filter` | `all`, `me`, `public`, `approval`, `workflow`, `archived` | Scope filter (server-side). |
| `--filter` | `all`, `public`, `approval`, `workflow`, `archived` | Scope filter (server-side). `me` still works but is deprecated. |
| `--owner` | UUID, email, or name (repeatable) | Filter by form owner(s). Max 50. (`CLI >= 3.5.2`) |
| `--order` | `alphabetical`, `recent`, `total` | Sort field (`total` = total response count). |
| `--ascending` / `--asc` | flag | Sort ascending (default: descending). |
| `--include-questions` | flag | Include question definitions in each form. |
Expand Down
2 changes: 2 additions & 0 deletions .agents/skills/dailybot/shared/list-query-and-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ In `--json` mode the error surfaces as `{ error, status, code, detail }`.
| `send_as_user_not_found` | The `--send-as-user` UUID doesn't resolve to a user. | Confirm the user exists (`dailybot user list`). |
| `invalid_kudos_filter` | `kudos list --filter` got an unrecognized value. | Use `received` or `given` (the CLI also accepts `KUDOS_RECEIVED` / `KUDOS_GIVEN`). |
| `send_message_validation_error` | `chat send` payload is missing content or otherwise invalid. | Read the `detail` — it names the problem (e.g. no message/buttons/image). |
| `invalid_owner_user_id` | `--owner` value isn't a valid UUID (after resolution). | Fix the UUID or name. (`CLI >= 3.5.2`) |
| `too_many_owner_user_ids` | More than 50 `--owner` values. | Narrow the filter — max 50 owners per request. (`CLI >= 3.5.2`) |

### 429 — rate limit

Expand Down
39 changes: 36 additions & 3 deletions dailybot_cli/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

MAX_FALLBACK_DETAIL_CHARS: int = 160 # cap for a non-JSON error body echoed to the user
MAX_SEARCH_QUERY_LENGTH: int = 256 # server rejects search queries longer than this
MAX_OWNER_USER_IDS: int = 50 # server rejects owner_user_ids lists longer than this


def _fallback_detail(response: httpx.Response) -> str:
Expand Down Expand Up @@ -865,6 +866,7 @@ def list_forms(
include_questions: bool = False,
include_archived: bool = False,
owner: str | None = None,
owner_user_ids: list[str] | None = None,
filter_scope: str | None = None,
order: str | None = None,
is_ascend: bool = False,
Expand All @@ -879,9 +881,10 @@ def list_forms(
) -> list[dict[str, Any]]:
"""GET /v1/forms/ — optionally expand questions, search, and page.

The default response is org-scoped (every form the caller can see on the
webapp list view). Pass ``owner="me"`` to narrow to the caller's own forms,
or ``filter_scope`` to apply a server-side scope filter.
The default response is org-wide (every form in the caller's org).
Capabilities are governed by each form's permissions. Pass
``owner_user_ids`` to filter by specific owners, or
``filter_scope`` to apply a server-side scope filter.

When ``meta`` is given it is populated with ``count`` / ``next`` for a
pagination footer.
Expand All @@ -893,6 +896,8 @@ def list_forms(
params["include_archived"] = "true"
if owner:
params["owner"] = owner
if owner_user_ids:
params["owner_user_ids"] = ",".join(owner_user_ids)
if filter_scope:
params["filter"] = filter_scope
if order:
Expand All @@ -911,6 +916,34 @@ def list_forms(
_fill_meta(meta, result)
return result.results

def list_form_owners(
self,
*,
search: str | None = None,
offset: int | None = None,
limit: int | None = None,
) -> dict[str, Any]:
"""GET /v1/forms/form-owners/ — paginated picker of org members who own forms.

Returns the raw paginated envelope ``{count, next, previous, results}``.
Each result has ``uuid``, ``full_name``, ``image``, ``role``, and
optionally ``email`` (only visible to admins/managers).
"""
params: dict[str, Any] = {}
if search:
params["search"] = search
if offset is not None:
params["offset"] = offset
if limit is not None:
params["limit"] = limit
response: httpx.Response = httpx.get(
f"{self.api_url}/v1/forms/form-owners/",
params=params,
headers=self._headers(),
timeout=self.timeout,
)
return self._handle_response(response)

def get_form(self, form_uuid: str) -> dict[str, Any]:
"""GET /v1/forms/<form_uuid>/ — form metadata and question definitions."""
response: httpx.Response = httpx.get(
Expand Down
158 changes: 151 additions & 7 deletions dailybot_cli/commands/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@

import click

from dailybot_cli.api_client import APIError, DailyBotClient
from dailybot_cli.api_client import (
MAX_OWNER_USER_IDS,
APIError,
DailyBotClient,
)
from dailybot_cli.commands.authoring_helpers import (
build_question,
build_question_edit_fields,
Expand All @@ -26,7 +30,9 @@
emit_json,
enforce_plan_access,
exit_for_api_error,
get_current_user_uuid,
require_auth,
resolve_user_by_name_or_uuid,
validate_user_filter,
)
from dailybot_cli.commands.query_options import (
Expand Down Expand Up @@ -64,6 +70,8 @@
_EMAIL_RE: re.Pattern[str] = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")

FORM_LIST_FILTERS: tuple[str, ...] = ("all", "me", "public", "approval", "workflow", "archived")
# "me" is deprecated server-side — kept here for back-compat but --mine now
# uses owner_user_ids with the caller's UUID instead.
FORM_LIST_ORDERS: tuple[str, ...] = ("alphabetical", "recent", "total")
RESPONSE_ORDERS: tuple[str, ...] = ("recent", "oldest")
RESPONSE_SOURCES: tuple[str, ...] = ("member", "anonymous", "automation", "public")
Expand Down Expand Up @@ -202,14 +210,26 @@ def form() -> None:
@click.option(
"--mine",
is_flag=True,
help="Only forms you own (default lists every form you can see in the org).",
help="Only forms you own (default lists every form in the org).",
)
@click.option(
"--owner",
"owners",
multiple=True,
help=(
"Filter by form owner (UUID, email, or name). Repeatable. "
"Names/emails are resolved via the org directory."
),
)
@click.option(
"--filter",
"filter_scope",
type=click.Choice(FORM_LIST_FILTERS, case_sensitive=False),
default=None,
help="Scope filter: all, me, public, approval, workflow, archived.",
help=(
"Scope filter: all, public, approval, workflow, archived. "
'("me" still works but is deprecated — use --mine or --owner instead.)'
),
)
@click.option(
"--order",
Expand All @@ -236,6 +256,7 @@ def form() -> None:
def form_list(
include_archived: bool,
mine: bool,
owners: tuple[str, ...],
filter_scope: str | None,
order: str | None,
is_ascend: bool,
Expand All @@ -255,15 +276,17 @@ def form_list(
"""List forms visible to you.

\b
Acts as you. You can only see and act on what you could in the webapp:
by default this is every form in your org you have list-view access to.
Pass --mine to narrow to only the forms you own, or --filter to scope.
Lists all forms in your organization. Capabilities (editing, response
visibility, state changes) are governed by each form's permissions.
Pass --mine or --owner to filter by form owner, or --filter to scope.
Archived forms are hidden unless you pass --include-archived or --filter archived.

\b
Examples:
dailybot form list
dailybot form list --mine
dailybot form list --owner "Jane Doe"
dailybot form list --owner <uuid> --owner <uuid>
dailybot form list --filter workflow --order alphabetical --asc
dailybot form list --filter approval
dailybot form list --filter public --order total
Expand All @@ -289,19 +312,140 @@ def form_list(
print_error(str(exc))
raise SystemExit(1)
client = require_auth()

owner_user_ids: list[str] | None = None
if mine or owners:
owner_user_ids = []
if mine:
my_uuid: str | None = get_current_user_uuid(client)
if my_uuid:
owner_user_ids.append(my_uuid)
else:
print_error(
"Could not resolve your user UUID for --mine. "
"Try passing your UUID with --owner instead."
)
raise SystemExit(1)
if owners:
owner_user_ids.extend(_resolve_owner_uuids(client, owners))
if len(owner_user_ids) > MAX_OWNER_USER_IDS:
print_error(
f"Too many owner UUIDs ({len(owner_user_ids)}). Maximum is {MAX_OWNER_USER_IDS}."
)
raise SystemExit(1)

execute_form_list(
client,
json_mode=json_mode,
include_archived=include_archived,
include_questions=include_questions,
owner="me" if mine else None,
owner_user_ids=owner_user_ids,
filter_scope=filter_scope,
order=order,
is_ascend=is_ascend,
spec=spec,
)


def _resolve_owner_uuids(
client: DailyBotClient,
identifiers: tuple[str, ...],
) -> list[str]:
"""Resolve owner identifiers (UUID, email, or name) to UUIDs.

Loads the org directory only when non-UUID identifiers are present.
"""
from dailybot_cli.commands.public_api_helpers import UUID_PATTERN

uuids: list[str] = []
to_resolve: list[str] = []
for ident in identifiers:
if UUID_PATTERN.match(ident):
uuids.append(ident)
else:
to_resolve.append(ident)
if to_resolve:
with console.status("Resolving owner names..."):
users: list[dict[str, Any]] = client.list_users(include_email=True)
for ident in to_resolve:
try:
uid, _name = resolve_user_by_name_or_uuid(users, ident)
uuids.append(uid)
except ValueError as exc:
print_error(str(exc))
raise SystemExit(1)
return uuids


@form.command("owners")
@click.option("--search", "-s", default=None, help="Search owners by name or email.")
@click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
@click.option("--limit", "-l", type=int, default=None, help="Max results (default 20, max 50).")
def form_owners(
search: str | None,
json_mode: bool,
limit: int | None,
) -> None:
"""List org members who own at least one form.

\b
A lightweight picker for the --owner filter on `form list`. Returns only
members who own at least one non-archived form. Email is visible only to
admins/managers.

\b
Examples:
dailybot form owners
dailybot form owners --search jane
dailybot form owners --json
"""
client = require_auth()
try:
with console.status("Fetching form owners..."):
data: dict[str, Any] = client.list_form_owners(
search=search,
limit=limit,
)
except APIError as exc:
exit_for_api_error(exc, json_mode)

results: list[dict[str, Any]] = data.get("results", [])
if json_mode:
emit_json(results)
return

if not results:
from dailybot_cli.display import print_info

print_info("No form owners found.")
return

from rich.table import Table

table = Table(title="Form owners", border_style="cyan")
table.add_column("Name", style="bold")
table.add_column("UUID", style="dim")
table.add_column("Role")
has_email: bool = any(r.get("email") for r in results)
if has_email:
table.add_column("Email")
for row in results:
cols: list[str] = [
str(row.get("full_name", "")),
str(row.get("uuid", "")),
str(row.get("role", "")),
]
if has_email:
cols.append(str(row.get("email", "")))
table.add_row(*cols)
console.print(table)
total: int | None = data.get("count")
if total is not None and total > len(results):
from dailybot_cli.display import print_info

print_info(f"Showing {len(results)} of {total} owners. Use --search to narrow.")


@form.command("get")
@click.argument("form_uuid")
@click.option("--json", "json_mode", is_flag=True, help="Emit machine-readable JSON to stdout.")
Expand Down
4 changes: 3 additions & 1 deletion dailybot_cli/commands/public_api_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,9 @@
"This form requires guest submitter info. "
"Add --guest-name and --guest-email to your command."
),
"invalid_filter": "Unknown --filter value. Use: all, me, public, approval, workflow, archived.",
"invalid_filter": "Unknown --filter value. Use: all, public, approval, workflow, archived.",
"invalid_owner_user_id": "Invalid --owner UUID format.",
"too_many_owner_user_ids": "Too many --owner UUIDs (max 50). Narrow your filter.",
"invalid_order": "Unknown --order value. Use: alphabetical, recent, total (forms) or recent, oldest (responses).",
"invalid_submission_sources": (
"Unknown --source value. Use: member, anonymous, automation, public (comma-separated)."
Expand Down
Loading