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.
Files changed (43) hide show
  1. clickup_cli-1.3.0/.claude/agents/api-compatibility-checker.md +39 -0
  2. clickup_cli-1.3.0/.claude/settings.json +44 -0
  3. clickup_cli-1.3.0/.claude/skills/changelog/SKILL.md +48 -0
  4. clickup_cli-1.3.0/.claude/skills/validate-output/SKILL.md +43 -0
  5. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/CHANGELOG.md +2 -0
  6. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/CLAUDE.md +40 -0
  7. clickup_cli-1.3.0/CONTRIBUTING.md +54 -0
  8. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/PKG-INFO +6 -6
  9. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/README.md +5 -5
  10. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/clickup-config.example.json +1 -4
  11. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/__init__.py +1 -1
  12. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/commands/init.py +0 -3
  13. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/commands/tasks.py +52 -50
  14. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/config.py +0 -3
  15. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/helpers.py +1 -2
  16. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/tests/conftest.py +2 -5
  17. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/tests/test_cli.py +46 -92
  18. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/tests/test_commands.py +109 -71
  19. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/tests/test_config.py +0 -13
  20. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/tests/test_helpers.py +2 -3
  21. clickup_cli-1.2.0/.claude/settings.json +0 -26
  22. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/.claude/agents/test-writer.md +0 -0
  23. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/.claude/skills/add-command.md +0 -0
  24. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/.claude/skills/clickup-cli.md +0 -0
  25. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/.claude/skills/release/SKILL.md +0 -0
  26. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/.github/workflows/ci.yml +0 -0
  27. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/.gitignore +0 -0
  28. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/.mcp.json +0 -0
  29. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/LICENSE +0 -0
  30. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/pyproject.toml +0 -0
  31. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/scripts/validate-cli-output.sh +0 -0
  32. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/__main__.py +0 -0
  33. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/cli.py +0 -0
  34. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/client.py +0 -0
  35. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/commands/__init__.py +0 -0
  36. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/commands/comments.py +0 -0
  37. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/commands/docs.py +0 -0
  38. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/commands/folders.py +0 -0
  39. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/commands/lists.py +0 -0
  40. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/commands/spaces.py +0 -0
  41. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/commands/tags.py +0 -0
  42. {clickup_cli-1.2.0 → clickup_cli-1.3.0}/src/clickup_cli/commands/team.py +0 -0
  43. {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.2.0
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
+ [![PyPI version](https://img.shields.io/pypi/v/clickup-cli)](https://pypi.org/project/clickup-cli/)
36
+ [![Python versions](https://img.shields.io/pypi/pyversions/clickup-cli)](https://pypi.org/project/clickup-cli/)
37
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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
+ [![PyPI version](https://img.shields.io/pypi/v/clickup-cli)](https://pypi.org/project/clickup-cli/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/clickup-cli)](https://pypi.org/project/clickup-cli/)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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,8 +5,5 @@
5
5
  "spaces": {
6
6
  "myspace": {"space_id": "SPACE_ID", "list_id": "DEFAULT_LIST_ID"}
7
7
  },
8
- "default_tags": [],
9
- "draft_tag": "draft",
10
- "good_as_is_tag": "good as is",
11
- "default_priority": 4
8
+ "default_tags": []
12
9
  }
@@ -1,3 +1,3 @@
1
1
  """ClickUp CLI — the missing ClickUp CLI for developers and AI agents."""
2
2
 
3
- __version__ = "1.2.0"
3
+ __version__ = "1.3.0"
@@ -132,9 +132,6 @@ def cmd_init(args):
132
132
  "user_id": user_id,
133
133
  "spaces": spaces,
134
134
  "default_tags": [],
135
- "draft_tag": "draft",
136
- "good_as_is_tag": "good as is",
137
- "default_priority": 4,
138
135
  }
139
136
 
140
137
  # Write config
@@ -5,7 +5,7 @@ import sys
5
5
 
6
6
  import requests
7
7
 
8
- from ..config import WORKSPACE_ID, SPACES, USER_ID, DEFAULT_TAGS, DRAFT_TAG, GOOD_AS_IS_TAG, DEFAULT_PRIORITY
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
- --good-as-is marks the task as intentionally simple (no draft tag).
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
- "--good-as-is",
238
- action="store_true",
239
- help="Mark task as intentionally simple (no draft tag)",
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 PER-39, JMP-12, MTM-8
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
- priority = _resolve_priority(args.priority) if args.priority else DEFAULT_PRIORITY
608
+ body = {"name": args.name}
609
+
610
+ if tags:
611
+ body["tags"] = tags
594
612
 
595
- body = {"name": args.name, "tags": tags, "priority": priority}
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
- if not args.no_assign:
604
- user_id = USER_ID
605
- if user_id:
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. PER-39)
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
- Agents naturally use flag forms (--task-id) while humans prefer positional.
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
- "personal": {"space_id": "333", "list_id": "444"},
16
- "jump": {"space_id": "555", "list_id": "666"},
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()