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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ A GitHub CLI-inspired command-line interface for ClickUp.
- **GitHub CLI-like Interface**: Familiar command structure for developers who use `gh`
- **Task Management**: Create, view, update, and manage tasks from the command line
- **Comment Management**: Add, list, and delete comments on tasks with user assignment
- **Custom Fields**: Read and write ClickUp custom fields, resolving names to option ids
- **Custom Fields**: Read and write ClickUp custom fields, resolving names to option ids, and find tasks by field value
- **Cache Management**: Optimize performance with intelligent caching and cache control commands
- **Project Configuration**: Set project-specific defaults with `.cu.yml` configuration files
- **API Passthrough**: Direct access to ClickUp API endpoints for advanced operations
Expand Down Expand Up @@ -109,6 +109,9 @@ cu field set <task-id> Machine "Chilastra,Sunrunner"

# Clear a value
cu field clear <task-id> "Last synced"

# Reverse lookup: find the task whose field holds a given value
cu field find Repo https://github.com/owner/repo
```

### Cache Management
Expand Down
4 changes: 3 additions & 1 deletion docs/site/commands/cu_export_tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ cu export tasks [flags]
-F, --file string Write to a file instead of stdout
-f, --format string Export format (csv, json, markdown) (default "csv")
-h, --help help for tasks
--include-closed Include closed tasks in the export
-l, --list string List ID to export tasks from
--priority string Filter by priority
-s, --space string Space ID to export tasks from
--status string Filter by status
--subtasks Include subtasks in the export
```

### Options inherited from parent commands
Expand All @@ -48,4 +50,4 @@ cu export tasks [flags]

* [cu export](cu_export.md) - Export data to various formats

###### Auto generated by spf13/cobra on 29-Aug-2026
###### Auto generated by spf13/cobra on 30-Aug-2026
1 change: 1 addition & 0 deletions docs/site/commands/cu_field.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ for dropdowns and labels — so names can be used on the command line.

* [cu](cu.md) - A GitHub CLI-inspired command-line interface for ClickUp
* [cu field clear](cu_field_clear.md) - Clear a custom field value on a task
* [cu field find](cu_field_find.md) - Find tasks whose custom field equals a value
* [cu field get](cu_field_get.md) - Show custom field values on a task
* [cu field list](cu_field_list.md) - List custom fields available on a list
* [cu field set](cu_field_set.md) - Set a custom field value on a task
Expand Down
43 changes: 43 additions & 0 deletions docs/site/commands/cu_field_find.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
## cu field find

Find tasks whose custom field equals a value

### Synopsis

Find tasks across the workspace whose custom field equals a value.

Filtering happens server-side via Get Filtered Team Tasks, so this does not walk
every list. This is the reverse lookup: from an external identifier stored in a
custom field back to the task that owns it.

A field id works anywhere. A field *name* has to be resolved against a list, so
pass --list or set a default; the resolved field itself is workspace-wide.

Examples:
cu field find Repo https://github.com/owner/repo
cu field find a5013cb5-9244-49be-a6a2-90e1cc8a1d31 https://github.com/owner/repo

```
cu field find <field> <value> [flags]
```

### Options

```
-h, --help help for find
-l, --list string List used to resolve a field name to an id (defaults to the configured default list)
```

### Options inherited from parent commands

```
--config string config file (default is $HOME/.config/cu/config.yaml)
--debug enable debug mode
-o, --output string output format (table|json|yaml|csv) (default "table")
```

### SEE ALSO

* [cu field](cu_field.md) - Manage custom field values

###### Auto generated by spf13/cobra on 28-Aug-2026
2 changes: 2 additions & 0 deletions docs/site/commands/cu_task_search.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ cu task search [query] [flags]

```
-h, --help help for search
--include-closed Include closed tasks in the search
--include-description Search in task descriptions as well as names
--limit int Maximum number of results to return (default 50)
-l, --list string Limit search to specific list
-s, --space string Limit search to specific space
--subtasks Include subtasks in the search
```

### Options inherited from parent commands
Expand Down
160 changes: 160 additions & 0 deletions internal/api/teamtasks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package api

import (
"context"
"fmt"

"github.com/raksul/go-clickup/clickup"
)

// tasksPerPage is the page size ClickUp uses for paged task endpoints. It is
// fixed server-side, so a short page means the last page.
const tasksPerPage = 100

// DefaultMaxTaskPages bounds pagination so a mistaken filter cannot walk an
// entire workspace forever under the rate limiter. Callers that genuinely want
// everything can raise it.
const DefaultMaxTaskPages = 50

// TeamTaskQuery filters a workspace-wide task search. Zero values mean "no
// filter", matching the API.
type TeamTaskQuery struct {
Statuses []string
Assignees []string
Tags []string
IncludeClosed bool
Subtasks bool
Archived bool
OrderBy string
Reverse bool

// CustomFields filters server-side on custom field values — the only way
// to answer "which task has Repo = X" without walking every list.
CustomFields clickup.CustomFieldsInGetTasksRequest

// MaxPages bounds pagination; zero means DefaultMaxTaskPages.
MaxPages int
}

func (q *TeamTaskQuery) toSDK(page int) *clickup.GetTasksOptions {
opts := &clickup.GetTasksOptions{
Page: page,
IncludeClosed: q.IncludeClosed,
Subtasks: q.Subtasks,
Archived: q.Archived,
OrderBy: q.OrderBy,
Reverse: q.Reverse,
}
if len(q.Statuses) > 0 {
opts.Statuses = q.Statuses
}
if len(q.Assignees) > 0 {
opts.Assignees = q.Assignees
}
if len(q.Tags) > 0 {
opts.Tags = q.Tags
}
if len(q.CustomFields) > 0 {
opts.CustomFields = q.CustomFields
}
return opts
}

// TaskPageResult reports what a paged fetch actually covered, so callers can
// tell "no more results" from "we stopped early".
type TaskPageResult struct {
Tasks []clickup.Task
Pages int
Truncated bool // hit MaxPages with a full page still coming back
}

// paginate walks pages until a short page (the last one) or maxPages, whichever
// comes first. Kept separate from the API calls so the termination and
// truncation rules are testable without a network round trip.
func paginate(maxPages int, fetch func(page int) ([]clickup.Task, error)) (*TaskPageResult, error) {
if maxPages <= 0 {
maxPages = DefaultMaxTaskPages
}

result := &TaskPageResult{}
for page := 0; page < maxPages; page++ {
tasks, err := fetch(page)
if err != nil {
return nil, err
}

result.Tasks = append(result.Tasks, tasks...)
result.Pages = page + 1

// A short page is the last page.
if len(tasks) < tasksPerPage {
return result, nil
}
}

// Stopped at the cap with a full page still arriving — the caller must be
// able to say so rather than present a partial set as complete.
result.Truncated = true
return result, nil
}

// SearchTeamTasks queries the workspace-wide Get Filtered Team Tasks endpoint,
// paging until exhaustion.
//
// This replaces walking workspace → spaces → folders → lists and calling
// GetTasks per list: that fan-out costs one request per list under a 100
// req/min limit, and it only ever fetched page 0, so any list past 100 tasks
// was silently cut off.
func (c *Client) SearchTeamTasks(ctx context.Context, teamID string, query *TeamTaskQuery) (*TaskPageResult, error) {
if query == nil {
query = &TeamTaskQuery{}
}
return paginate(query.MaxPages, func(page int) ([]clickup.Task, error) {
if err := c.rateLimiter.Wait(ctx); err != nil {
return nil, err
}
tasks, _, err := c.client.Tasks.GetFilteredTeamTasks(ctx, teamID, query.toSDK(page))
if err != nil {
return nil, c.handleError(err)
}
return tasks, nil
})
}

// ListTasksAllPages pages a single list to exhaustion. GetTasks fetches only
// the page it is asked for, so callers that want a whole list need this.
func (c *Client) ListTasksAllPages(ctx context.Context, listID string, options *TaskQueryOptions, maxPages int) (*TaskPageResult, error) {
if options == nil {
options = &TaskQueryOptions{}
}
return paginate(maxPages, func(page int) ([]clickup.Task, error) {
opts := *options
opts.Page = page
return c.GetTasks(ctx, listID, &opts)
})
}

// FindTasksByCustomField returns the tasks in a workspace whose custom field
// equals value — the reverse lookup from an external identifier back to a task.
func (c *Client) FindTasksByCustomField(ctx context.Context, teamID, fieldID, value string) ([]clickup.Task, error) {
if fieldID == "" {
return nil, fmt.Errorf("a custom field id is required")
}

query := &TeamTaskQuery{
IncludeClosed: true, // a match must not depend on the task being open
CustomFields: clickup.CustomFieldsInGetTasksRequest{
{
FieldId: fieldID,
Operator: clickup.Equals,
Value: []string{value},
},
},
}

result, err := c.SearchTeamTasks(ctx, teamID, query)
if err != nil {
return nil, err
}
return result.Tasks, nil
}
Loading
Loading