This document contains important context about the Things CLI project for AI assistants and future development.
Things CLI is a command-line interface for Things 3 (macOS/iOS task manager) that provides both read and write capabilities through a hybrid architecture.
- Repository: https://github.com/skempken/things-cli
- Language: Python 3.10+
- Package Manager: uv
- License: MIT
- Author: Sebastian Kempken
The CLI uses two different APIs because Things 3 has distinct capabilities:
-
Write Operations → Things URL Scheme
- Create, update, delete tasks/projects
- Requires
THINGS_TOKENenvironment variable for modifications - Uses
subprocessto executeopen things:///...URLs - Supports dry-run mode
-
Read Operations → JXA (JavaScript for Automation)
- Query tasks, projects, areas, tags
- Requires Things 3 to be running
- Uses
osascript -l JavaScript - Returns JSON output
Why this architecture?
- Things 3 AppleScript/JXA API is intentionally read-only
- URL Scheme is the only way to write data
- Neither API can do both operations
things-cli/
├── things.py # Main CLI (643 lines)
│ ├── URL Scheme commands (add, update, show, search, json)
│ ├── JXA list command
│ ├── Import/export functionality
│ └── Typer CLI app with subcommands
│
├── things_jxa.py # JXA Helper Module (387 lines)
│ ├── run_jxa() - Execute JXA scripts
│ ├── get_list_tasks() - Query built-in lists
│ ├── get_tasks_by_tag/area/project() - Filtering
│ └── get_all_tags/areas/projects() - Metadata
│
├── pyproject.toml # uv project config
├── README.md # User documentation
├── LICENSE # MIT License
├── .gitignore # Excludes .venv, .env, Python cache
└── CLAUDE.md # This file
Problem: The list command function shadowed Python's built-in list type, causing:
isinstance(value, list) # TypeError: isinstance() arg 2 must be a typeSolution: Changed to:
hasattr(value, '__iter__') and not isinstance(value, str)Problem: Tasks showed + instead of spaces (e.g., "CLI+Test:+Task")
Solution: Added quote_via=quote to urlencode():
query_string = urlencode(encoded_params, safe='', quote_via=quote)Things 3 supports multiple languages, and the CLI now auto-detects or accepts explicit locale settings:
LOCALE_MAPPINGS = {
"de": {"inbox": "Eingang", "today": "Heute", ...},
"en": {"inbox": "Inbox", "today": "Today", ...},
}Implementation:
--locale de/en: Uses hardcoded mappings (fast, ~0ms overhead)--locale autoor omitted: Auto-detects via JXA (~100-500ms first call)detect_list_names(): Queries Things 3 to identify localized list names
Usage:
uv run things list today # Auto-detect locale
uv run things list today --locale de # Force German
uv run things list today --locale en # Force EnglishEnglish commands map to localized list names automatically based on the detected or specified locale.
All list commands use print() instead of console.print() to ensure valid JSON without ANSI codes.
-
Tags in JSON imports must exist - Cannot create new tags via JSON
- Use
things add --tags new-tagfirst to create tags - Then use JSON import with existing tags
- Use
-
Rate limiting: Maximum 250 items per 10 seconds
-
Auth token required for:
updateandupdate-projectcommandsjson-commandandimportcommands- Set via
THINGS_TOKENenvironment variable
- Read-only - Cannot modify any data
- Requires Things 3 to be running
- Synchronous - Can be slow for large datasets
- No pagination - Must fetch all matching items
- macOS only - JXA is not available on other platforms
# Required for write operations (update, json import)
export THINGS_TOKEN="your-token-here"Get token from: Things → Settings → General → Enable Things URLs → Manage
cd things-cli
uv pip install -e .After installation with uv pip install -e ., you have these options:
# Option 1: Via uv run (recommended, automatically manages venv)
uv run things <command>
# Option 2: Activate venv first, then use command directly
source .venv/bin/activate
things <command>
deactivate
# Option 3: Run script directly without installation
uv run things.py <command>Note: The things command is only available within the virtual environment created by uv. Use uv run things for the best experience.
Always use --dry-run first:
uv run things add --title "Test Task" --dry-run
# Check URL, then run without --dry-run# JXA operations are read-only, safe to run anytime
uv run things list today
uv run things list tags# 1. Query available tags
uv run things list tags | grep "time"
# 2. Get today's tasks
uv run things list today > tasks.json
# 3. Update a task with tag
uv run things update --id <UUID> --add-tags "1h"
# 4. Verify update
uv run things list todayNote: All commands below assume you're using uv run things or have activated the virtual environment.
-
things add --title "Test" --dry-run- Shows correct URL -
things add --title "Test"- Creates task in Things -
things add-project- Creates project -
things update- Updates existing task (requires token) -
things show --id today- Opens Today view -
things search- Opens search - Spaces encoded as
%20, not+
-
things list today- Returns valid JSON -
things list tags- Lists all tags -
things list areas- Lists areas with counts -
things list projects- Lists projects -
things list --tag work- Filters by tag -
things list --area Personal- Filters by area
-
things export test.json --type task- Creates template -
things import test.json- Imports tasks (requires token) - JSON with existing tags works
- JSON with new tags fails (expected)
The list command function can shadow Python's list type in some contexts. Always use fully qualified names or hasattr checks.
URL Scheme: Can create new tags automatically
uv run things add --title "Test" --tags "new-tag" # Creates "new-tag"JSON Import: Cannot create new tags
uv run things import tasks.json # Fails if tags don't existThings accepts multiple date formats:
yyyy-mm-dd(e.g.,2024-12-31)- Natural language (e.g.,
today,tomorrow,in 3 days) - With time:
yyyy-mm-dd@HH:MM
To get task UUIDs:
- Mac: Control-click → Share → Copy Link
- iOS: Open item → Share → Copy Link
- Extract UUID from
things:///show?id=<UUID>
This project follows a structured issue-to-merge workflow for all feature development and bug fixes.
1. Issue Creation → 2. Feature Branch → 3. Development → 4. Push & PR → 5. Merge & Close
Before starting any work, create a GitHub issue to:
- Document the problem or feature request
- Discuss implementation approaches
- Get alignment on the solution
- Track progress publicly
Creating an issue:
# Using GitHub CLI
gh issue create \
--title "Support non-German Things 3 localizations in JXA operations" \
--body "Description of problem and proposed solutions..."
# Or via web interface
# https://github.com/skempken/things-cli/issues/newIssue template structure:
- Problem: Clear description of the issue
- Impact: Who is affected and how
- Proposed Solutions: Multiple approaches with pros/cons
- Implementation Details: Code locations, technical strategy
- Acceptance Criteria: Definition of done
- References: Related docs, code, issues
Example: See Issue #1 for localization
Never commit directly to main. Always work on a feature branch:
# Create and switch to feature branch
git checkout -b feature/issue-1-localization
# Or for bug fixes
git checkout -b fix/issue-2-error-handling
# Or for documentation
git checkout -b docs/update-readmeBranch naming conventions:
feature/<issue-number>-<short-description>- New featuresfix/<issue-number>-<short-description>- Bug fixesdocs/<description>- Documentation onlyrefactor/<description>- Code refactoringtest/<description>- Test additions/fixes
Examples:
feature/1-localization-supportfix/3-url-encoding-spacesdocs/api-examplesrefactor/jxa-error-handling
Work on your feature branch, making commits as you go:
# Make changes
vim things_jxa.py
# Test your changes
uv run things.py list today
# Stage and commit
git add things_jxa.py
git commit -m "feat: add locale auto-detection for JXA lists
Detect Things 3 locale by querying list names via JXA.
Falls back to English if detection fails.
Caches result in ~/.things-cli/locale.cache
Fixes #1
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>"Commit message format: See Git Workflow section below
Important:
- Keep commits atomic (one logical change per commit)
- Write descriptive commit messages
- Reference the issue number with
Fixes #NorRelates to #N - Test your changes before committing
- Use
--dry-runflags where available
When your feature is complete and tested:
# Push feature branch to remote
git push -u origin feature/1-localization-support
# Create pull request using GitHub CLI
gh pr create \
--title "feat: Add locale auto-detection for JXA operations" \
--body "## Summary
- Auto-detect Things 3 locale from list names
- Cache detected locale in ~/.things-cli/locale.cache
- Fallback to English if detection fails
- Backwards compatible with existing German setup
## Changes
- Modified \`things_jxa.py\` to detect locale dynamically
- Added caching mechanism for performance
- Updated tests for multiple locales
- Documented locale behavior in README
## Testing
- [x] Tested with English Things 3
- [x] Tested with German Things 3
- [x] Verified cache creation and reuse
- [x] All existing tests pass
Closes #1
🤖 Generated with [Claude Code](https://claude.com/claude-code)"
# Or via web interface
open https://github.com/skempken/things-cli/compare/feature/1-localization-supportPull Request Guidelines:
- Reference the original issue with
Closes #NorFixes #N - Provide clear summary of changes
- Include testing checklist
- Add screenshots/examples if UI changes
- Request review if working with collaborators
After review and CI passes:
# Merge via GitHub CLI (squash merge recommended for clean history)
gh pr merge --squash --delete-branch
# Or use GitHub web interface
# Merge pull request → Squash and merge → Delete branchThe issue will close automatically if your PR/commit message included:
Closes #NFixes #NResolves #N
Post-merge cleanup:
# Switch back to main
git checkout main
# Pull latest changes
git pull origin main
# Delete local feature branch (if not already done)
git branch -d feature/1-localization-supportDO:
- ✅ Create issue before starting work
- ✅ Use descriptive branch names with issue numbers
- ✅ Make small, focused commits
- ✅ Test thoroughly before pushing
- ✅ Reference issues in commits and PRs
- ✅ Delete branches after merging
- ✅ Keep PRs focused on one issue
DON'T:
- ❌ Commit directly to
main - ❌ Work without an issue (for non-trivial changes)
- ❌ Create PRs with unrelated changes
- ❌ Skip testing
- ❌ Leave stale branches
- ❌ Forget to reference the issue number
# Full workflow example
gh issue create --title "..." --body "..." # 1. Create issue (#N)
git checkout -b feature/N-description # 2. Create branch
# ... make changes, test ...
git add . && git commit -m "feat: ... Fixes #N" # 3. Commit
git push -u origin feature/N-description # 4. Push
gh pr create --title "..." --body "... Closes #N" # 5. Create PR
gh pr merge --squash --delete-branch # 6. Merge & close
git checkout main && git pull # 7. Update mainFor critical bugs in production:
# Create hotfix branch from main
git checkout -b hotfix/critical-bug main
# Fix, test, commit
git add . && git commit -m "fix: critical bug..."
# Push and create PR immediately
git push -u origin hotfix/critical-bug
gh pr create --title "HOTFIX: ..." --body "..."
# Fast-track review and merge
gh pr merge --squash --delete-branchFollow conventional commits with Claude Code footer:
<type>: <description>
<body>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
ad48840- Initial commit103b695- Add MIT Licensedc0c584- Add read operations via JXA180afe1- Fix isinstance bug in build_url
- ✅ URL Scheme write operations (add, update, show, search, json)
- ✅ JXA read operations (list, query, filter)
- ✅ JSON import/export
- ✅ Dry-run mode
- ✅ Rich terminal output
- ✅ Multi-locale support with auto-detection (Issue #1)
-
--format tablefor Rich table output (currently only JSON) -
--format plainfor simple text lists - Advanced filters:
--overdue,--created-after,--completed-today - Sorting:
--sort-by due_date - Pagination:
--limit 10,--offset 20 - Batch update operations (update multiple tasks at once)
- Configuration file support (
~/.things-cli/config.yaml) - Shell completion (bash, zsh, fish)
- Task templates (YAML/TOML files for common task structures)
- ❌ Delete operations (Things URL Scheme doesn't support it)
- ❌ Bulk operations without rate limits (250 items/10s max)
- ❌ Real-time sync monitoring (no webhook/event API)
- ❌ Cross-platform support (Things is macOS/iOS only)
[project.dependencies]
typer = ">=0.12.0" # CLI framework
rich = ">=13.0.0" # Terminal formattingBoth are well-maintained and stable. No known compatibility issues.
-
Auth Token: Never commit
THINGS_TOKENto git- Added to
.gitignoreas.env - Stored in environment variable only
- Added to
-
Task IDs: UUIDs are not secret but are user-specific
- Safe to show in examples/docs
- But don't expose personal task content
-
JXA Scripts: Executed with user privileges
- No elevated permissions needed
- Scripts are inline, not from external files
# Use dry-run to see generated URL
uv run things add --title "Test" --dry-run
# Check URL encoding
python3 -c "from urllib.parse import quote; print(quote('test string'))"# Test JXA directly
osascript -l JavaScript -e 'Application("Things3").name()'
# Check Things is running
ps aux | grep Things
# Test Python import
python3 -c "import things_jxa; print(things_jxa.get_all_tags())"# Validate JSON
python3 -m json.tool < file.json
# Check for non-existent tags
uv run things list tags | grep "tag-name"- JXA calls: ~100-500ms per query (depends on data size)
- URL Scheme: Near-instant (opens URL handler)
- Large lists: JXA can take 1-2 seconds for 100+ items
- Batch imports: Limited to 250 items, takes ~1 second
The project was tested with:
- 4 tasks in Today
- 65 tasks in Upcoming
- 74 tags (including priority, time, location, person tags)
- 8 areas
- 25 projects
All operations worked correctly with German-localized Things 3.
- Things URL Scheme Docs: https://culturedcode.com/things/support/articles/2803573/
- JXA Guide: Search Apple's official JXA docs
- uv Documentation: https://github.com/astral-sh/uv
- GitHub Issues: https://github.com/skempken/things-cli/issues
- Date: 2025-10-30
- Last Commit: 180afe1
- Things CLI Version: 0.1.0
- Tested with: Things 3 on macOS (German and English localization via --locale)
- Documentation: Added comprehensive Development Workflow section and multi-locale support (Issue #1)