diff --git a/README.md b/README.md index 2edf736..8808739 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/docs/commands.md b/docs/commands.md index b66fee9..8345e49 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -665,6 +665,8 @@ cup doc-pages abc123 --json ### `cup folders ` 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 diff --git a/skills/clickup-cli/SKILL.md b/skills/clickup-cli/SKILL.md index 82556ac..1bf20c2 100644 --- a/skills/clickup-cli/SKILL.md +++ b/skills/clickup-cli/SKILL.md @@ -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 [--name partial] [--archived]` | Lists in a space (including folder lists) | -| `cup folders [--name partial] [--archived]` | Folders in a space (with their lists) | +| `cup folders [--name partial] [--archived]` | Folders in a space (with their lists); JSON includes `parent_folder` for subfolders | | `cup time-in-status ` | 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 | diff --git a/src/api.ts b/src/api.ts index 65c95dd..191b7e4 100644 --- a/src/api.ts +++ b/src/api.ts @@ -136,6 +136,7 @@ export interface ListWithStatuses extends List { interface Folder { id: string name: string + parent_folder?: string } export interface ViewSummary { diff --git a/src/commands/folders.ts b/src/commands/folders.ts index cc86c61..4dae375 100644 --- a/src/commands/folders.ts +++ b/src/commands/folders.ts @@ -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 }> } @@ -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 } diff --git a/tests/unit/api.test.ts b/tests/unit/api.test.ts index 3af619b..0a0a5f2 100644 --- a/tests/unit/api.test.ts +++ b/tests/unit/api.test.ts @@ -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') }) diff --git a/tests/unit/commands/folders.test.ts b/tests/unit/commands/folders.test.ts index 9d3dfe2..693c361 100644 --- a/tests/unit/commands/folders.test.ts +++ b/tests/unit/commands/folders.test.ts @@ -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) }) @@ -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') }) }) @@ -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 () => {