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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ Full CRUD for the core ClickUp workflow:
| ⭐ **Favorites** | Local favorites for quick access to sprint folders, spaces, lists, folders, views, tasks |
| 👁️ **Views** | List, get, create, update, delete views on lists |
| 🔗 **Webhooks** | List, create, update, delete webhooks; scope to space, folder, list, or task |
| 🏢 **Workspace** | Spaces, folders, lists (full CRUD + rename + from template), members, user groups, task types, templates, plan, shared hierarchy |
| 🏢 **Workspace** | Spaces, folders, lists (full CRUD + rename + from template; subfolder parent IDs in JSON), members, user groups, task types, templates, plan, shared hierarchy |
| 📎 **Attachments** | Upload files to tasks, list task attachments, shown in detail views |

[Full API coverage details](docs/api-coverage.md) | [Command reference](docs/commands.md)
Expand Down
2 changes: 2 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,8 @@ cup doc-pages abc123 --json
### `cup folders <spaceId>`

List folders in a space with their contained lists. Useful for discovering folder and list IDs.
JSON output remains a flat array and includes `parent_folder` on subfolders so clients can
reconstruct the hierarchy. Top-level folders omit the field.

```bash
cup folders <spaceId>
Expand Down
2 changes: 1 addition & 1 deletion skills/clickup-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ All commands support `--help` for full flag details. All commands support `--jso
| `cup overdue [--all] [--include-closed]` | Tasks past due date (most overdue first) |
| `cup spaces [--name partial] [--my] [--archived]` | List/filter workspace spaces |
| `cup lists <spaceId> [--name partial] [--archived]` | Lists in a space (including folder lists) |
| `cup folders <spaceId> [--name partial] [--archived]` | Folders in a space (with their lists) |
| `cup folders <spaceId> [--name partial] [--archived]` | Folders in a space (with their lists); JSON includes `parent_folder` for subfolders |
| `cup time-in-status <id>` | Show how long a task has been in each status |
| `cup members` | Workspace members (username, ID, email) |
| `cup groups` | User groups/teams (handle, name, UUID, member count) for `--group-assignee` flags |
Expand Down
1 change: 1 addition & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ export interface ListWithStatuses extends List {
interface Folder {
id: string
name: string
parent_folder?: string
}

export interface ViewSummary {
Expand Down
8 changes: 7 additions & 1 deletion src/commands/folders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { Config } from '../config.js'
interface FolderWithLists {
id: string
name: string
parent_folder?: string
lists: Array<{ id: string; name: string }>
}

Expand All @@ -24,7 +25,12 @@ export async function listFolders(
const results: FolderWithLists[] = []
for (const folder of filtered) {
const lists = await client.getFolderLists(folder.id)
results.push({ id: folder.id, name: folder.name, lists })
results.push({
id: folder.id,
name: folder.name,
...(folder.parent_folder !== undefined ? { parent_folder: folder.parent_folder } : {}),
lists,
})
}
return results
}
Expand Down
16 changes: 14 additions & 2 deletions tests/unit/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,9 +477,21 @@ describe('ClickUpClient', () => {
})

it('getFolders returns folders for a space', async () => {
mockFetch.mockReturnValue(mockResponse({ folders: [{ id: 'f1', name: 'Q1 Work' }] }))
mockFetch.mockReturnValue(
mockResponse({
folders: [
{ id: 'f1', name: 'Q1 Work' },
{ id: 'f2', name: 'Q1 Work Subfolder', parent_folder: 'f1' },
],
}),
)
const folders = await client.getFolders('s1')
expect(folders).toEqual([{ id: 'f1', name: 'Q1 Work' }])
expect(folders).toEqual([
{ id: 'f1', name: 'Q1 Work' },
{ id: 'f2', name: 'Q1 Work Subfolder', parent_folder: 'f1' },
])
expect(folders[0]).not.toHaveProperty('parent_folder')
expect(folders[1]).toHaveProperty('parent_folder', 'f1')
expect(String(mockFetch.mock.calls[0]![0])).toContain('/space/s1/folder')
})

Expand Down
36 changes: 25 additions & 11 deletions tests/unit/commands/folders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,27 @@ describe('listFolders', () => {
it('returns folders with their lists', async () => {
mockGetFolders.mockResolvedValue([
{ id: 'f1', name: 'Sprint Folder' },
{ id: 'f2', name: 'Backlog' },
{ id: 'f2', name: 'Sprint Subfolder', parent_folder: 'f1' },
])
mockGetFolderLists.mockResolvedValueOnce([{ id: 'l1', name: 'Sprint 1' }])
mockGetFolderLists.mockResolvedValueOnce([{ id: 'l2', name: 'Backlog Items' }])
mockGetFolderLists.mockResolvedValueOnce([{ id: 'l2', name: 'Sprint 2' }])
const { listFolders } = await import('../../../src/commands/folders.js')
const result = await listFolders(mockConfig, 's1')
expect(result).toEqual([
{ id: 'f1', name: 'Sprint Folder', lists: [{ id: 'l1', name: 'Sprint 1' }] },
{ id: 'f2', name: 'Backlog', lists: [{ id: 'l2', name: 'Backlog Items' }] },
{
id: 'f1',
name: 'Sprint Folder',
lists: [{ id: 'l1', name: 'Sprint 1' }],
},
{
id: 'f2',
name: 'Sprint Subfolder',
parent_folder: 'f1',
lists: [{ id: 'l2', name: 'Sprint 2' }],
},
])
expect(result[0]).not.toHaveProperty('parent_folder')
expect(result[1]).toHaveProperty('parent_folder', 'f1')
expect(mockGetFolders).toHaveBeenCalledWith('s1', undefined)
expect(mockGetFolderLists).toHaveBeenCalledTimes(2)
})
Expand Down Expand Up @@ -72,12 +83,14 @@ describe('formatFolders', () => {
it('shows folder names with list names', async () => {
const { formatFolders } = await import('../../../src/commands/folders.js')
const result = formatFolders([
{ id: 'f1', name: 'Sprint', lists: [{ id: 'l1', name: 'Sprint 1' }] },
{
id: 'f2',
name: 'Sprint',
parent_folder: 'f1',
lists: [{ id: 'l1', name: 'Sprint 1' }],
},
])
expect(result).toContain('Sprint')
expect(result).toContain('Sprint 1')
expect(result).toContain('f1')
expect(result).toContain('l1')
expect(result).toBe('Sprint f2\n > Sprint 1 l1')
})
})

Expand All @@ -91,15 +104,16 @@ describe('formatFoldersMarkdown', () => {
const { formatFoldersMarkdown } = await import('../../../src/commands/folders.js')
const result = formatFoldersMarkdown([
{
id: 'f1',
id: 'f2',
name: 'Sprint',
parent_folder: 'f1',
lists: [
{ id: 'l1', name: 'Sprint 1' },
{ id: 'l2', name: 'Sprint 2' },
],
},
])
expect(result).toBe('- **Sprint** (f1)\n - Sprint 1 (l1)\n - Sprint 2 (l2)')
expect(result).toBe('- **Sprint** (f2)\n - Sprint 1 (l1)\n - Sprint 2 (l2)')
})

it('handles folders without lists', async () => {
Expand Down