clickup-cli 1.2.0__tar.gz → 1.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- clickup_cli-1.3.0/.claude/agents/api-compatibility-checker.md +39 -0
- clickup_cli-1.3.0/.claude/settings.json +44 -0
- clickup_cli-1.3.0/.claude/skills/changelog/SKILL.md +48 -0
- clickup_cli-1.3.0/.claude/skills/validate-output/SKILL.md +43 -0
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/CHANGELOG.md +2 -0
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/CLAUDE.md +40 -0
- clickup_cli-1.3.0/CONTRIBUTING.md +54 -0
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/PKG-INFO +6 -6
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/README.md +5 -5
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/clickup-config.example.json +1 -4
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/__init__.py +1 -1
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/commands/init.py +0 -3
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/commands/tasks.py +52 -50
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/config.py +0 -3
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/helpers.py +1 -2
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/tests/conftest.py +2 -5
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/tests/test_cli.py +46 -92
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/tests/test_commands.py +109 -71
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/tests/test_config.py +0 -13
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/tests/test_helpers.py +2 -3
- clickup_cli-1.2.0/.claude/settings.json +0 -26
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/.claude/agents/test-writer.md +0 -0
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/.claude/skills/add-command.md +0 -0
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/.claude/skills/clickup-cli.md +0 -0
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/.claude/skills/release/SKILL.md +0 -0
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/.github/workflows/ci.yml +0 -0
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/.gitignore +0 -0
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/.mcp.json +0 -0
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/LICENSE +0 -0
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/pyproject.toml +0 -0
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/scripts/validate-cli-output.sh +0 -0
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/__main__.py +0 -0
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/cli.py +0 -0
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/client.py +0 -0
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/commands/__init__.py +0 -0
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/commands/comments.py +0 -0
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/commands/docs.py +0 -0
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/commands/folders.py +0 -0
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/commands/lists.py +0 -0
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/commands/spaces.py +0 -0
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/commands/tags.py +0 -0
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/commands/team.py +0 -0
- {clickup_cli-1.2.0 → clickup_cli-1.3.0}/tests/test_client.py +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: api-compatibility-checker
|
|
3
|
+
description: Cross-reference CLI endpoint usage against ClickUp API docs to flag deprecated or changed endpoints
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# API Compatibility Checker
|
|
7
|
+
|
|
8
|
+
You verify that the ClickUp CLI's API endpoint usage is current and compatible.
|
|
9
|
+
|
|
10
|
+
## Steps
|
|
11
|
+
|
|
12
|
+
1. Read `src/clickup_cli/client.py` to extract all API endpoints used (URL patterns in requests calls).
|
|
13
|
+
|
|
14
|
+
2. Read each command file in `src/clickup_cli/commands/` to find any direct endpoint references.
|
|
15
|
+
|
|
16
|
+
3. Build a list of all endpoints used, grouped by API version (v2 vs v3).
|
|
17
|
+
|
|
18
|
+
4. For each endpoint, check the current ClickUp API documentation to verify:
|
|
19
|
+
- The endpoint still exists
|
|
20
|
+
- The HTTP method matches
|
|
21
|
+
- Required parameters haven't changed
|
|
22
|
+
- Response format is consistent with what the CLI expects
|
|
23
|
+
|
|
24
|
+
5. Report findings:
|
|
25
|
+
- **OK** — endpoint verified as current
|
|
26
|
+
- **WARNING** — endpoint exists but has changes (new required params, deprecated fields)
|
|
27
|
+
- **BREAKING** — endpoint removed or fundamentally changed
|
|
28
|
+
|
|
29
|
+
## Output
|
|
30
|
+
|
|
31
|
+
Return a JSON summary:
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"total_endpoints": 0,
|
|
35
|
+
"ok": 0,
|
|
36
|
+
"warnings": [],
|
|
37
|
+
"breaking": []
|
|
38
|
+
}
|
|
39
|
+
```
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PostToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "Edit|Write",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "filepath=\"$TOOL_INPUT_FILE_PATH$TOOL_INPUT_file_path\"; if echo \"$filepath\" | grep -qE '\\.py$'; then ruff check --fix --quiet \"$filepath\" 2>/dev/null; ruff format --quiet \"$filepath\" 2>/dev/null; fi; true"
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"matcher": "Edit|Write",
|
|
15
|
+
"hooks": [
|
|
16
|
+
{
|
|
17
|
+
"type": "command",
|
|
18
|
+
"command": "filepath=\"$TOOL_INPUT_FILE_PATH$TOOL_INPUT_file_path\"; if echo \"$filepath\" | grep -qE 'src/clickup_cli/commands/[a-z]+\\.py$'; then python -m pytest tests/test_commands.py -x -q --tb=short 2>&1 | tail -5; elif echo \"$filepath\" | grep -qE 'src/clickup_cli/(cli|helpers|config|client)\\.py$'; then mod=$(basename \"$filepath\" .py); if [ -f \"tests/test_${mod}.py\" ]; then python -m pytest \"tests/test_${mod}.py\" -x -q --tb=short 2>&1 | tail -5; fi; fi; true"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"PreToolUse": [
|
|
24
|
+
{
|
|
25
|
+
"matcher": "Edit|Write",
|
|
26
|
+
"hooks": [
|
|
27
|
+
{
|
|
28
|
+
"type": "command",
|
|
29
|
+
"command": "filepath=\"$TOOL_INPUT_FILE_PATH$TOOL_INPUT_file_path\"; if echo \"$filepath\" | grep -qE '\\.(env|secret|pem|key)$|credentials'; then echo 'BLOCKED: refusing to edit sensitive file'; exit 1; fi"
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"matcher": "Edit|Write",
|
|
35
|
+
"hooks": [
|
|
36
|
+
{
|
|
37
|
+
"type": "command",
|
|
38
|
+
"command": "filepath=\"$TOOL_INPUT_FILE_PATH$TOOL_INPUT_file_path\"; if echo \"$filepath\" | grep -qE 'pyproject\\.toml$'; then echo 'BLOCKED: pyproject.toml edit requires explicit approval'; exit 1; fi"
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: changelog
|
|
3
|
+
description: Generate release notes from git log since last tag, categorized by commit type
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Generate Changelog Entry
|
|
8
|
+
|
|
9
|
+
Generate a categorized changelog entry for the next release.
|
|
10
|
+
|
|
11
|
+
## Steps
|
|
12
|
+
|
|
13
|
+
1. Find the last release tag:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
git describe --tags --abbrev=0 2>/dev/null || echo "none"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
2. Get all commits since that tag (or all commits if no tag exists):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
git log $(git describe --tags --abbrev=0 2>/dev/null)..HEAD --oneline 2>/dev/null || git log --oneline
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
3. Categorize commits by their prefix:
|
|
26
|
+
- **Added** — `feat:`, `add:`
|
|
27
|
+
- **Changed** — `refactor:`, `update:`, `improve:`
|
|
28
|
+
- **Fixed** — `fix:`, `bugfix:`
|
|
29
|
+
- **Testing** — `test:`
|
|
30
|
+
- **Docs** — `docs:`
|
|
31
|
+
- **Other** — everything else
|
|
32
|
+
|
|
33
|
+
4. Draft a markdown changelog entry:
|
|
34
|
+
|
|
35
|
+
```markdown
|
|
36
|
+
## [vX.Y.Z] - YYYY-MM-DD
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
- Description of new features
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
- Description of changes
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
- Description of bug fixes
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
5. Present the draft for review. Do not write to any file until the user approves.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: validate-output
|
|
3
|
+
description: Verify the CLI's JSON-only stdout contract across all commands. Run after adding new commands or modifying output logic.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Validate CLI Output Contract
|
|
7
|
+
|
|
8
|
+
Verify that every CLI command produces valid JSON on stdout and errors on stderr.
|
|
9
|
+
|
|
10
|
+
## Steps
|
|
11
|
+
|
|
12
|
+
1. Get all command groups and subcommands:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
clickup --help 2>/dev/null
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
2. For each command group, get its subcommands:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
clickup <group> --help 2>/dev/null
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
3. For each subcommand that supports `--dry-run`, run it with dummy arguments and `--dry-run`:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
clickup --dry-run <group> <subcommand> <required-args> 2>/dev/null
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
4. Validate that stdout is valid JSON by piping through `python3 -c "import sys,json; json.load(sys.stdin)"`.
|
|
31
|
+
|
|
32
|
+
5. For read-only commands (list, get, search), run with `--help` and verify the help text renders without errors.
|
|
33
|
+
|
|
34
|
+
6. Report results as a summary:
|
|
35
|
+
- Total commands checked
|
|
36
|
+
- Commands producing valid JSON
|
|
37
|
+
- Commands with issues (list each with the error)
|
|
38
|
+
|
|
39
|
+
## What Counts as a Failure
|
|
40
|
+
|
|
41
|
+
- Any stdout output that is not valid JSON
|
|
42
|
+
- Any command that crashes (non-zero exit without stderr message)
|
|
43
|
+
- Help text that fails to render
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
## 1.2.0 (2026-03-29)
|
|
4
4
|
|
|
5
|
+
**First PyPI release** — `pip install clickup-cli`
|
|
6
|
+
|
|
5
7
|
- Accept flag aliases for all positional arguments — agents can now use `--task-id`, `--query`, `--doc-id`, `--page-id`, `--folder-id`, `--list-id`, `--comment-id`, `--space` instead of positional args. Both forms work; positional args are unchanged for backwards compatibility.
|
|
6
8
|
- Auto-infer `--space` from `--list` on `tasks create` — when `--list` is provided without `--space`, the CLI fetches the list metadata via API to resolve its parent space automatically. Eliminates the most common agent error (12+ failures in 3 days).
|
|
7
9
|
- Make `--space` optional on `tasks create` (was required) — now only required if `--list` is also absent.
|
|
@@ -54,8 +54,27 @@ When adding new commands with ID arguments, always use `add_id_argument()` inste
|
|
|
54
54
|
pip install -e ".[dev]"
|
|
55
55
|
pytest -v
|
|
56
56
|
ruff check src/ tests/
|
|
57
|
+
scripts/validate-cli-output.sh # verify JSON stdout contract
|
|
57
58
|
```
|
|
58
59
|
|
|
60
|
+
## Test Structure
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
tests/
|
|
64
|
+
├── conftest.py # test config setup (runs at import time, before clickup_cli loads)
|
|
65
|
+
├── test_cli.py # argument parsing, dispatch, global flags, resolve_id_args
|
|
66
|
+
├── test_client.py # ClickUpClient: rate limiting, dry-run, debug mode, HTTP methods
|
|
67
|
+
├── test_commands.py # all command handlers: tasks, comments, docs, folders, lists, spaces, tags, team
|
|
68
|
+
├── test_config.py # config loading: file, env vars, fallback chain, workspace auto-detect
|
|
69
|
+
└── test_helpers.py # output/error helpers, compact_task, add_id_argument, fetch_all_comments pagination
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
305 tests, all using `unittest.TestCase` + `FakeClient` pattern (no real HTTP calls).
|
|
73
|
+
|
|
74
|
+
## CI
|
|
75
|
+
|
|
76
|
+
GitHub Actions (`ci.yml`): lint + test on Python 3.9, 3.11, 3.13. Runs on push to main and PRs.
|
|
77
|
+
|
|
59
78
|
## Adding a New Command
|
|
60
79
|
|
|
61
80
|
1. Create or extend a file in `src/clickup_cli/commands/`
|
|
@@ -74,3 +93,24 @@ ruff check src/ tests/
|
|
|
74
93
|
- Stdout is always JSON. Errors and warnings go to stderr.
|
|
75
94
|
- Every mutation supports `--dry-run`
|
|
76
95
|
- Config is lazy-loaded — the `init` command works without any config file
|
|
96
|
+
|
|
97
|
+
## Automations
|
|
98
|
+
|
|
99
|
+
Hooks, skills, and subagents in `.claude/`:
|
|
100
|
+
|
|
101
|
+
**Hooks** (`settings.json`):
|
|
102
|
+
- PostToolUse: ruff auto-lint/format on `.py` edits
|
|
103
|
+
- PostToolUse: auto-run affected test file on source edits
|
|
104
|
+
- PreToolUse: block `.env`/`.pem`/`.key`/credentials edits
|
|
105
|
+
- PreToolUse: block `pyproject.toml` edits (requires explicit approval)
|
|
106
|
+
|
|
107
|
+
**Skills** (`skills/`):
|
|
108
|
+
- `add-command` — step-by-step workflow for adding new CLI commands
|
|
109
|
+
- `clickup-cli` — usage guide for the CLI itself
|
|
110
|
+
- `release` — version bump, build, PyPI publish workflow
|
|
111
|
+
- `validate-output` — verify JSON stdout contract across all commands
|
|
112
|
+
- `changelog` — generate categorized release notes from git log
|
|
113
|
+
|
|
114
|
+
**Subagents** (`agents/`):
|
|
115
|
+
- `test-writer` — generate pytest tests following existing patterns
|
|
116
|
+
- `api-compatibility-checker` — cross-ref CLI endpoints against ClickUp API docs
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for your interest in clickup-cli.
|
|
4
|
+
|
|
5
|
+
## Getting Started
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
git clone https://github.com/efetoker/clickup-cli.git
|
|
9
|
+
cd clickup-cli
|
|
10
|
+
pip install -e ".[dev]"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Running Tests
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pytest -v
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
All tests must pass before submitting a PR.
|
|
20
|
+
|
|
21
|
+
## Linting
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
ruff check src/ tests/
|
|
25
|
+
ruff format src/ tests/
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Guidelines
|
|
29
|
+
|
|
30
|
+
- **JSON stdout, errors to stderr** — all commands follow this convention
|
|
31
|
+
- **Dry-run on mutations** — every mutating command must support `--dry-run`
|
|
32
|
+
- **Help text is documentation** — `--help` should be self-sufficient for discovering usage
|
|
33
|
+
- **No workspace-specific values** — help text and source must not contain hardcoded workspace IDs, space names, or user data
|
|
34
|
+
- **Tests required** — new commands need test coverage in `tests/`
|
|
35
|
+
|
|
36
|
+
## Adding a New Command
|
|
37
|
+
|
|
38
|
+
1. Create or extend a file in `src/clickup_cli/commands/`
|
|
39
|
+
2. Implement `register_parser()` and handler functions in the same module
|
|
40
|
+
3. Register the handler in `commands/__init__.py`
|
|
41
|
+
4. Use `add_id_argument()` from `helpers.py` for positional ID arguments (provides both positional and `--flag` forms)
|
|
42
|
+
5. Add `--help` text with description and usage examples
|
|
43
|
+
6. Add tests in `tests/`
|
|
44
|
+
|
|
45
|
+
## Submitting Changes
|
|
46
|
+
|
|
47
|
+
1. Fork the repo and create a feature branch
|
|
48
|
+
2. Make your changes with tests
|
|
49
|
+
3. Run `pytest -v` and `ruff check src/ tests/`
|
|
50
|
+
4. Open a PR with a clear description of what changed and why
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
|
|
54
|
+
By contributing, you agree that your contributions will be licensed under the MIT License.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clickup-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: The missing ClickUp CLI. Built for developers and AI agents.
|
|
5
5
|
Project-URL: Homepage, https://github.com/efetoker/clickup-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/efetoker/clickup-cli
|
|
@@ -32,6 +32,10 @@ Description-Content-Type: text/markdown
|
|
|
32
32
|
|
|
33
33
|
# clickup-cli
|
|
34
34
|
|
|
35
|
+
[](https://pypi.org/project/clickup-cli/)
|
|
36
|
+
[](https://pypi.org/project/clickup-cli/)
|
|
37
|
+
[](LICENSE)
|
|
38
|
+
|
|
35
39
|
The missing ClickUp CLI. Built for developers and AI agents.
|
|
36
40
|
|
|
37
41
|
There's no official ClickUp CLI. If you're a developer who lives in the terminal, or an AI agent that needs structured data from ClickUp, this fills the gap. JSON stdout, errors to stderr, dry-run on every mutation.
|
|
@@ -127,7 +131,6 @@ clickup tasks list --space <name> --pretty
|
|
|
127
131
|
- **`tasks create`** auto-infers `--space` from `--list` via API lookup. You can omit `--space` if `--list` is provided.
|
|
128
132
|
- **`tasks get`** auto-fetches comments and appends them to the output. Use `--no-comments` to skip.
|
|
129
133
|
- **`tasks search`** auto-detects task ID patterns like `PROJ-39` and applies prefix filtering.
|
|
130
|
-
- **`tasks create`** checks for duplicates before creating. Use `--skip-dedup` to bypass.
|
|
131
134
|
- **`docs edit-page --append`** reads the current page content, appends your new content, and sends one update.
|
|
132
135
|
- **Tag names** are auto-lowercased (ClickUp API stores them lowercase regardless of UI display).
|
|
133
136
|
- **Doc ID ≠ page ID.** Always use `docs pages <doc_id>` to discover page IDs before using `get-page` or `edit-page`.
|
|
@@ -146,10 +149,7 @@ clickup tasks list --space <name> --pretty
|
|
|
146
149
|
"spaces": {
|
|
147
150
|
"myspace": {"space_id": "111", "list_id": "222"}
|
|
148
151
|
},
|
|
149
|
-
"default_tags": []
|
|
150
|
-
"draft_tag": "draft",
|
|
151
|
-
"good_as_is_tag": "good as is",
|
|
152
|
-
"default_priority": 4
|
|
152
|
+
"default_tags": []
|
|
153
153
|
}
|
|
154
154
|
```
|
|
155
155
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# clickup-cli
|
|
2
2
|
|
|
3
|
+
[](https://pypi.org/project/clickup-cli/)
|
|
4
|
+
[](https://pypi.org/project/clickup-cli/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
3
7
|
The missing ClickUp CLI. Built for developers and AI agents.
|
|
4
8
|
|
|
5
9
|
There's no official ClickUp CLI. If you're a developer who lives in the terminal, or an AI agent that needs structured data from ClickUp, this fills the gap. JSON stdout, errors to stderr, dry-run on every mutation.
|
|
@@ -95,7 +99,6 @@ clickup tasks list --space <name> --pretty
|
|
|
95
99
|
- **`tasks create`** auto-infers `--space` from `--list` via API lookup. You can omit `--space` if `--list` is provided.
|
|
96
100
|
- **`tasks get`** auto-fetches comments and appends them to the output. Use `--no-comments` to skip.
|
|
97
101
|
- **`tasks search`** auto-detects task ID patterns like `PROJ-39` and applies prefix filtering.
|
|
98
|
-
- **`tasks create`** checks for duplicates before creating. Use `--skip-dedup` to bypass.
|
|
99
102
|
- **`docs edit-page --append`** reads the current page content, appends your new content, and sends one update.
|
|
100
103
|
- **Tag names** are auto-lowercased (ClickUp API stores them lowercase regardless of UI display).
|
|
101
104
|
- **Doc ID ≠ page ID.** Always use `docs pages <doc_id>` to discover page IDs before using `get-page` or `edit-page`.
|
|
@@ -114,10 +117,7 @@ clickup tasks list --space <name> --pretty
|
|
|
114
117
|
"spaces": {
|
|
115
118
|
"myspace": {"space_id": "111", "list_id": "222"}
|
|
116
119
|
},
|
|
117
|
-
"default_tags": []
|
|
118
|
-
"draft_tag": "draft",
|
|
119
|
-
"good_as_is_tag": "good as is",
|
|
120
|
-
"default_priority": 4
|
|
120
|
+
"default_tags": []
|
|
121
121
|
}
|
|
122
122
|
```
|
|
123
123
|
|
|
@@ -5,7 +5,7 @@ import sys
|
|
|
5
5
|
|
|
6
6
|
import requests
|
|
7
7
|
|
|
8
|
-
from ..config import WORKSPACE_ID, SPACES,
|
|
8
|
+
from ..config import WORKSPACE_ID, SPACES, DEFAULT_TAGS
|
|
9
9
|
from ..helpers import read_content, error, format_tasks, fetch_all_comments, add_id_argument
|
|
10
10
|
|
|
11
11
|
|
|
@@ -66,7 +66,9 @@ Use --subtasks to include nested child tasks in the results. Without it,
|
|
|
66
66
|
only top-level tasks are returned (ClickUp API default).
|
|
67
67
|
|
|
68
68
|
Use this when you need to see all tasks, optionally filtered by status
|
|
69
|
-
or including closed tasks.
|
|
69
|
+
or including closed tasks.
|
|
70
|
+
|
|
71
|
+
Use --tag to filter by tag name (API-level filtering, exact match).""",
|
|
70
72
|
epilog="""\
|
|
71
73
|
returns:
|
|
72
74
|
{"tasks": [...], "count": N}
|
|
@@ -78,6 +80,7 @@ examples:
|
|
|
78
80
|
clickup tasks list --space <name> --include-closed
|
|
79
81
|
clickup tasks list --space <name> --status "in progress"
|
|
80
82
|
clickup tasks list --space <name> --subtasks
|
|
83
|
+
clickup tasks list --space <name> --tag "created by claude"
|
|
81
84
|
|
|
82
85
|
notes:
|
|
83
86
|
Output is compact by default (id, name, status, priority, url).
|
|
@@ -114,6 +117,13 @@ notes:
|
|
|
114
117
|
action="store_true",
|
|
115
118
|
help="Include subtasks (nested child tasks) in results",
|
|
116
119
|
)
|
|
120
|
+
tl.add_argument(
|
|
121
|
+
"--tag",
|
|
122
|
+
type=str,
|
|
123
|
+
action="append",
|
|
124
|
+
dest="tags",
|
|
125
|
+
help="Filter by tag name (repeatable, API-level, auto-lowercased)",
|
|
126
|
+
)
|
|
117
127
|
tl.add_argument(
|
|
118
128
|
"--fields",
|
|
119
129
|
type=str,
|
|
@@ -172,8 +182,6 @@ Create a new task in a list. This is a mutating command.
|
|
|
172
182
|
By default, the task is created in the space's default list. Use --list
|
|
173
183
|
to target a specific list instead (e.g. one inside a folder).
|
|
174
184
|
|
|
175
|
-
Tags are applied automatically based on config (default_tags, draft_tag).
|
|
176
|
-
|
|
177
185
|
Use --desc for inline text or --desc-file for file-based content.
|
|
178
186
|
Do not use both at the same time.
|
|
179
187
|
|
|
@@ -190,16 +198,13 @@ examples:
|
|
|
190
198
|
clickup tasks create --space <name> --name "Fix bug" --desc "Details here"
|
|
191
199
|
clickup tasks create --space <name> --list 12345 --name "In folder list"
|
|
192
200
|
clickup tasks create --space <name> --name "Read article" --desc-file notes.md
|
|
193
|
-
clickup --dry-run tasks create --space <name> --name "Test" --good-as-is
|
|
194
201
|
|
|
195
202
|
notes:
|
|
196
203
|
--space is always required (to resolve target list). --list is optional.
|
|
197
204
|
If --list is given, the task is created in that list instead of the
|
|
198
205
|
space's default list.
|
|
199
206
|
--desc and --desc-file are mutually exclusive. Using both is an error.
|
|
200
|
-
--
|
|
201
|
-
--no-assign skips the default assignee.
|
|
202
|
-
--priority defaults to the value in your config (default: low).
|
|
207
|
+
--assign assigns the task to a user ID.
|
|
203
208
|
Does not support: checklists, custom fields, attachments, or due dates.""",
|
|
204
209
|
)
|
|
205
210
|
tc.add_argument(
|
|
@@ -232,17 +237,11 @@ notes:
|
|
|
232
237
|
type=str,
|
|
233
238
|
help="Priority: urgent, high, normal, low (default: from config)",
|
|
234
239
|
)
|
|
235
|
-
tc.add_argument("--no-assign", action="store_true", help="Skip default assignee")
|
|
236
240
|
tc.add_argument(
|
|
237
|
-
"--
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
tc.add_argument(
|
|
242
|
-
"--skip-dedup",
|
|
243
|
-
action="store_true",
|
|
244
|
-
default=False,
|
|
245
|
-
help="Skip duplicate check and create the task even if one with the same name exists",
|
|
241
|
+
"--assign",
|
|
242
|
+
type=str,
|
|
243
|
+
dest="assign_user",
|
|
244
|
+
help="Assign to a user ID",
|
|
246
245
|
)
|
|
247
246
|
|
|
248
247
|
# tasks update
|
|
@@ -354,6 +353,13 @@ notes:
|
|
|
354
353
|
type=str,
|
|
355
354
|
help="Keep only tasks whose name starts with this prefix (client-side filter)",
|
|
356
355
|
)
|
|
356
|
+
ts.add_argument(
|
|
357
|
+
"--tag",
|
|
358
|
+
type=str,
|
|
359
|
+
action="append",
|
|
360
|
+
dest="tags",
|
|
361
|
+
help="Filter by tag name (repeatable, client-side, auto-lowercased)",
|
|
362
|
+
)
|
|
357
363
|
ts.add_argument(
|
|
358
364
|
"--fields",
|
|
359
365
|
type=str,
|
|
@@ -452,7 +458,7 @@ examples:
|
|
|
452
458
|
|
|
453
459
|
PRIORITY_MAP = {"urgent": 1, "high": 2, "normal": 3, "low": 4}
|
|
454
460
|
|
|
455
|
-
# Pattern for task ID queries like
|
|
461
|
+
# Pattern for task ID queries like PROJ-39, BUG-12
|
|
456
462
|
_TASK_ID_PATTERN = re.compile(r"^[A-Z]+-\d+$")
|
|
457
463
|
|
|
458
464
|
|
|
@@ -510,6 +516,15 @@ def _paginate_tasks(client, path, params):
|
|
|
510
516
|
return all_tasks
|
|
511
517
|
|
|
512
518
|
|
|
519
|
+
def _filter_by_tags(tasks, tag_names):
|
|
520
|
+
"""Client-side filter: keep tasks that have ALL specified tags."""
|
|
521
|
+
required = {t.lower() for t in tag_names}
|
|
522
|
+
return [
|
|
523
|
+
t for t in tasks
|
|
524
|
+
if required <= {tg.get("name", "").lower() for tg in t.get("tags", [])}
|
|
525
|
+
]
|
|
526
|
+
|
|
527
|
+
|
|
513
528
|
def cmd_tasks_list(client, args):
|
|
514
529
|
list_id = _resolve_list_id(args)
|
|
515
530
|
if client.dry_run:
|
|
@@ -522,6 +537,10 @@ def cmd_tasks_list(client, args):
|
|
|
522
537
|
params["statuses[]"] = args.status
|
|
523
538
|
if args.subtasks:
|
|
524
539
|
params["subtasks"] = "true"
|
|
540
|
+
tag_filter = getattr(args, "tags", None)
|
|
541
|
+
if tag_filter:
|
|
542
|
+
for tag in tag_filter:
|
|
543
|
+
params["tags[]"] = tag.lower()
|
|
525
544
|
|
|
526
545
|
all_tasks = _paginate_tasks(client, f"/list/{list_id}/task", params)
|
|
527
546
|
return _format_and_wrap(all_tasks, args)
|
|
@@ -585,14 +604,14 @@ def cmd_tasks_create(client, args):
|
|
|
585
604
|
desc = read_content(args.desc, args.desc_file, "--desc")
|
|
586
605
|
|
|
587
606
|
tags = list(DEFAULT_TAGS) # copy to avoid mutating config
|
|
588
|
-
if args.good_as_is:
|
|
589
|
-
tags.append(GOOD_AS_IS_TAG)
|
|
590
|
-
elif not desc:
|
|
591
|
-
tags.append(DRAFT_TAG)
|
|
592
607
|
|
|
593
|
-
|
|
608
|
+
body = {"name": args.name}
|
|
609
|
+
|
|
610
|
+
if tags:
|
|
611
|
+
body["tags"] = tags
|
|
594
612
|
|
|
595
|
-
|
|
613
|
+
if args.priority:
|
|
614
|
+
body["priority"] = _resolve_priority(args.priority)
|
|
596
615
|
|
|
597
616
|
if desc:
|
|
598
617
|
body["markdown_description"] = desc
|
|
@@ -600,10 +619,9 @@ def cmd_tasks_create(client, args):
|
|
|
600
619
|
if args.status:
|
|
601
620
|
body["status"] = args.status
|
|
602
621
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
body["assignees"] = [int(user_id)]
|
|
622
|
+
assign_user = getattr(args, "assign_user", None)
|
|
623
|
+
if assign_user:
|
|
624
|
+
body["assignees"] = [int(assign_user)]
|
|
607
625
|
|
|
608
626
|
if client.dry_run:
|
|
609
627
|
return {
|
|
@@ -613,26 +631,6 @@ def cmd_tasks_create(client, args):
|
|
|
613
631
|
"list_id": list_id,
|
|
614
632
|
}
|
|
615
633
|
|
|
616
|
-
# Pre-create duplicate search
|
|
617
|
-
if not getattr(args, "skip_dedup", False):
|
|
618
|
-
search_resp = client.get_v2(
|
|
619
|
-
f"/team/{WORKSPACE_ID}/task",
|
|
620
|
-
params={"search": args.name, "list_ids[]": list_id},
|
|
621
|
-
)
|
|
622
|
-
existing = [
|
|
623
|
-
t for t in search_resp.get("tasks", [])
|
|
624
|
-
if t.get("name", "").lower() == args.name.lower()
|
|
625
|
-
]
|
|
626
|
-
if existing:
|
|
627
|
-
match = existing[0]
|
|
628
|
-
print(
|
|
629
|
-
f"warning: found existing task with same name: "
|
|
630
|
-
f"{match.get('id')} — {match.get('url', 'no url')}",
|
|
631
|
-
file=sys.stderr,
|
|
632
|
-
)
|
|
633
|
-
match["duplicate_of"] = match["id"]
|
|
634
|
-
return match
|
|
635
|
-
|
|
636
634
|
return client.post_v2(f"/list/{list_id}/task", data=body)
|
|
637
635
|
|
|
638
636
|
|
|
@@ -660,7 +658,7 @@ def cmd_tasks_search(client, args):
|
|
|
660
658
|
if client.dry_run:
|
|
661
659
|
return {"dry_run": True, "action": "search_tasks", "query": args.query}
|
|
662
660
|
|
|
663
|
-
# Auto-apply --name-prefix when query looks like a task ID (e.g.
|
|
661
|
+
# Auto-apply --name-prefix when query looks like a task ID (e.g. PROJ-39)
|
|
664
662
|
name_prefix = getattr(args, "name_prefix", None)
|
|
665
663
|
if not name_prefix and _TASK_ID_PATTERN.match(args.query):
|
|
666
664
|
name_prefix = args.query
|
|
@@ -691,6 +689,10 @@ def cmd_tasks_search(client, args):
|
|
|
691
689
|
if task.get("name", "").startswith(name_prefix)
|
|
692
690
|
]
|
|
693
691
|
|
|
692
|
+
tag_filter = getattr(args, "tags", None)
|
|
693
|
+
if tag_filter:
|
|
694
|
+
all_tasks = _filter_by_tags(all_tasks, tag_filter)
|
|
695
|
+
|
|
694
696
|
return _format_and_wrap(all_tasks, args)
|
|
695
697
|
|
|
696
698
|
|
|
@@ -187,9 +187,6 @@ _ATTR_MAP = {
|
|
|
187
187
|
"USER_ID": lambda c: c.get("user_id", ""),
|
|
188
188
|
"SPACES": lambda c: c.get("spaces", {}),
|
|
189
189
|
"DEFAULT_TAGS": lambda c: c.get("default_tags", []),
|
|
190
|
-
"DRAFT_TAG": lambda c: c.get("draft_tag", "draft"),
|
|
191
|
-
"GOOD_AS_IS_TAG": lambda c: c.get("good_as_is_tag", "good as is"),
|
|
192
|
-
"DEFAULT_PRIORITY": lambda c: c.get("default_priority", 4),
|
|
193
190
|
}
|
|
194
191
|
|
|
195
192
|
|
|
@@ -8,8 +8,7 @@ import sys
|
|
|
8
8
|
def add_id_argument(parser, name, help_text):
|
|
9
9
|
"""Add an argument that accepts both positional and --flag forms.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
This adds both so either style works.
|
|
11
|
+
This adds both so either style works (e.g. `command abc123` or `command --task-id abc123`).
|
|
13
12
|
|
|
14
13
|
Example:
|
|
15
14
|
add_id_argument(parser, 'task_id', 'ClickUp task ID')
|
|
@@ -12,13 +12,10 @@ _test_config = {
|
|
|
12
12
|
"user_id": "12345",
|
|
13
13
|
"spaces": {
|
|
14
14
|
"testspace": {"space_id": "111", "list_id": "222"},
|
|
15
|
-
"
|
|
16
|
-
"
|
|
15
|
+
"dev": {"space_id": "333", "list_id": "444"},
|
|
16
|
+
"staging": {"space_id": "555", "list_id": "666"},
|
|
17
17
|
},
|
|
18
18
|
"default_tags": [],
|
|
19
|
-
"draft_tag": "draft",
|
|
20
|
-
"good_as_is_tag": "good as is",
|
|
21
|
-
"default_priority": 4,
|
|
22
19
|
}
|
|
23
20
|
|
|
24
21
|
_test_dir = tempfile.mkdtemp()
|