An open API service indexing awesome lists of open source software.

https://github.com/superbereza/things-cli

CLI for Things 3 (read inbox/today/projects, search, add/complete/update todos) — JSON output, ships an agent skill
https://github.com/superbereza/things-cli

ai-tools cli macos productivity python skills task-management things3

Last synced: 5 days ago
JSON representation

CLI for Things 3 (read inbox/today/projects, search, add/complete/update todos) — JSON output, ships an agent skill

Awesome Lists containing this project

README

          

# things-cli

A small Python CLI for [Things 3](https://culturedcode.com/things/) — read your inbox / today / upcoming / projects / areas / tags, search, add todos and projects, complete, cancel, update, batch-edit. Outputs JSON, designed for piping into `jq` or being driven by an AI agent.

Ships an agent **skill** at [`skills/things/SKILL.md`](skills/things/SKILL.md) so an agent can read and update Things tasks automatically — run `things today` proactively at the start of a session and reach for `things add` / `things complete` when the conversation turns to tasks. The same skill is wired up for **Claude Code, Cursor, Codex and Gemini** from one source (see [Install](#install)).

## How it works

- **Reads** go through [`thingsapi/things.py`](https://github.com/thingsapi/things.py), which queries the local Things SQLite database directly (fast, no AppleScript round-trips).
- **Writes** go through the [Things URL scheme](https://culturedcode.com/things/help/url-scheme/) via `osascript` so Things processes them natively.
- **Auth token** for `update` / `complete` / `cancel` / `tag-*` is fetched via `things.token()`.

CLI logic is a small Python package (`src/things_cli/`, declared in `pyproject.toml` with a `things` entry point). `bin/things` is a thin self-bootstrapping launcher that creates a dedicated `.venv/`, installs the package editable on first run, and execs the `things` entry point (works whether called directly, via symlink, or from a plugin dir). Both `uv` and stdlib `venv` + `pip` are supported.

## Install

Requirements:

- Python 3.10+
- macOS (Things 3 is Mac-only)
- Things 3 with URL scheme enabled — open Things → Settings → General → "Enable Things URLs"

Install via the plugin. The `.venv/` is built automatically on first run.

### 1. As a Claude Code plugin (this repo is its own marketplace)

```text
/plugin marketplace add superbereza/things-cli
/plugin install things@things-cli
```

Claude pulls the repo, loads `skills/things/SKILL.md`, and the first `things …` call builds the venv. The plugin's `bin/` is auto-added to PATH, so the skill just calls `things`.

### 2. From an aggregate marketplace

If this plugin is listed in a shared marketplace (e.g. `superbereza/superbereza-skills`):

```text
/plugin marketplace add superbereza/superbereza-skills
/plugin install things@superbereza-skills
```

### Other agents

The same `skills/` directory is exposed to **Cursor** (`.cursor-plugin/`), **Codex** (`.codex-plugin/`) and **Gemini** (`gemini-extension.json` → [`AGENTS.md`](AGENTS.md)). One skill, one source of truth — see [`AGENTS.md`](AGENTS.md).

Run `things doctor --pretty` after install to verify everything's wired up:

```
✓ things.py library ok version 1.0.1
✓ Things 3 app ok version 3.22.11
✓ SQLite read access ok reads work — 65 inbox items
✓ Auth token ok 22-char token available

Overall: ALL OK
```

## Usage

### Reads

```bash
things today # JSON array
things today --pretty # indented
things today --grep "report" --limit 5
things inbox --grep "buy"
things projects --items # include each project's task titles
things areas --items # include each area's projects
things tags
things search "meeting" --limit 10
```

| Flag | On which read | Effect |
|---|---|---|
| `--pretty`, `-p` | all | Indent JSON |
| `--grep PATTERN`, `-g` | all except `search` | Case-insensitive substring filter on title |
| `--limit N` | all | Truncate to N items |
| `--items`, `-i` | `projects`, `areas` | Include children titles |

### Single writes

```bash
things add "Buy milk"
things add "Read article" --notes "https://..." --when today --tags reading
things add "Task" --list "Work" --tags "urgent,important"
things add "Trip prep" \
--checklist "Pack toothbrush" --checklist "Charge headphones"

# Verify the write actually landed and capture the real UUID:
things add "Buy milk" --when today --tags shopping --wait
# → {"status":"ok","title":"Buy milk","uuid":"3aEmFg6pBm1Vihy5DofYdR"}

# Add a project with initial tasks
things add-project "Renovate kitchen" --area "Home" \
--todos "Measure walls" --todos "Pick paint color"

# Move / update
things update --title "New title"
things update --list "Other Project" # move (name OR UUID)
things update --list MDs59magzJkPhhpPSnZSq # UUID also works
things update --when tomorrow --deadline 2026-07-01

# Complete / cancel — both accept multiple UUIDs
things complete
things cancel

# Tags — preserve existing
things tag-add urgent reviewed
things tag-remove draft

# Open something in the Things UI
things show today
things show
```

### Batch writes via `--stdin`

`things add --stdin` accepts either a JSON array or NDJSON (one JSON object per line) — saves N round-trips when adding many items:

```bash
echo '[
{"title": "Email Alice", "when": "today", "tags": "comms"},
{"title": "Review PR #1234", "when": "today"},
{"title": "Pay credit card", "deadline": "2026-06-15", "list": "Personal"}
]' | things add --stdin --wait --pretty
```

Returns an array of `{status, title, uuid}` records (`uuid` populated only with `--wait`).

NDJSON form is also accepted:

```bash
{"title": "T1"}
{"title": "T2", "deadline": "2026-07-01"}
{"title": "T3", "list": "Work"}
```

Per-item fields supported (mirror the single-add flags): `title` (required), `notes`, `when`, `deadline`, `tags` (string, comma-separated), `list` (name OR UUID), `checklist` (list of strings).

### Diagnostics

```bash
things --version
things doctor # JSON
things doctor --pretty # human-readable table
```

`doctor` checks: things.py library, Things 3 app reachable, SQLite reads, auth token. Exit code 1 if anything fails.

## Output contract (for agents / scripts)

Reads return a JSON **array** of items; the full schema is documented in [skills/things/SKILL.md](skills/things/SKILL.md#output-contract).

Common todo fields:

```json
{
"uuid": "3aEmFg6pBm1Vihy5DofYdR", // 22-char base62, stable
"title": "Buy milk",
"type": "to-do", // "to-do" | "project"
"status": "incomplete", // "incomplete" | "completed" | "canceled"
"list": "Inbox", // smart-list this todo is parked in
"notes": "...", // optional
"start_date": "2026-06-02", // optional
"deadline": "2026-06-10", // optional
"tags": ["urgent", "work"], // optional
"project": "Work", // optional, resolved name
"project_uuid": "...", // optional, paired
"area": "Home", // optional, resolved name
"area_uuid": "..." // optional, paired
}
```

Writes return a status object:

```json
{ "status": "ok", "title": "Buy milk", "uuid": "..." }
{ "status": "ok", "uuid": "...", "completed": true }
{ "status": "ok", "uuid": "...", "tags": ["urgent", "reviewed"] }
```

Errors are always structured (exit code 1):

```json
{ "status": "error", "code": "INVALID_UUID" | "MISSING_TITLE" | "NOT_FOUND" | ..., "message": "..." }
```

## Subcommand reference

| Command | Action | Needs `things.py` lib |
|---|---|---|
| `inbox`, `today`, `upcoming`, `anytime`, `someday` | List todos in that smart list | yes |
| `projects [--items]` | List projects (and their tasks) | yes |
| `areas [--items]` | List areas (and their projects) | yes |
| `tags` | List all tags | yes |
| `search ` | Substring search | yes |
| `add [...]` | Add a todo | no (URL scheme) |
| `add --stdin` | Add many todos (JSON array or NDJSON) | no |
| `add-project [...]` | Add a project | no |
| `update [...]` | Update existing todo (incl. move via `--list`) | yes (auth token) |
| `complete [...]` | Mark complete (✓) | yes (auth token) |
| `cancel [...]` | Mark canceled (✗, won't do) | yes (auth token) |
| `tag-add ...` | Add tags, preserve existing | yes |
| `tag-remove ...` | Remove specific tags, keep the rest | yes |
| `show ` | Open in Things app UI | no |
| `doctor` | Self-check installation | mostly no |

Run `things --help` for per-command flags.

## Caveats

- **Writes are fire-and-forget** unless you pass `--wait`. The CLI returns `{"status":"ok"}` as soon as the URL is dispatched; if Things rejects the URL, the CLI never sees the error. `--wait` polls SQLite for up to ~3 s to confirm.
- **No `trash`** subcommand — Things' URL scheme can't delete. `cancel` marks "won't do" (✗), `complete` marks done (✓). Real deletion is GUI-only.
- **Tag mutations** through Things' URL scheme always replace the full set; `tag-add` / `tag-remove` work around this by reading existing tags first via `things.py` and sending the merged set.
- **UUIDs are 22-char base62.** The validator on `complete` / `cancel` / `update` / `tag-*` rejects anything else with a clear `INVALID_UUID` error — usually the cause is forgetting to leave a shell variable unquoted: use `things complete $uuids`, not `things complete "$uuids"`.
- **`--grep` filters in Python** after fetching everything from SQLite. Fine for the typical Things database (hundreds of items), not a substitute for SQL pushdown on huge collections.

## The skill

The skill lives at `skills/things/SKILL.md` — the single source of truth, shared
across agents:

- **Claude Code** — the plugin install exposes it. New sessions pick it up (the
current one does not — skills load at session start).
- **Cursor / Codex** — `.cursor-plugin/plugin.json` and `.codex-plugin/plugin.json`
point at the same `./skills/`.
- **Gemini** — reads [`GEMINI.md`](GEMINI.md) (declared in `gemini-extension.json`).

The skill description tells the agent to use `things` whenever the user mentions
tasks/today/inbox/etc., and to run `things today` proactively at the start of a
session.

## Uninstall

`/plugin uninstall things@things-cli`. (If you cloned the repo, just delete the clone — its `.venv/` lives inside it.)

## Credits

CLI logic ported from [hald/things-mcp](https://github.com/hald/things-mcp) (an MCP server for Things 3). Uses [thingsapi/things.py](https://github.com/thingsapi/things.py) for SQLite reads.

## OpenCode

This skill also supports [OpenCode](https://opencode.ai) — see [`.opencode/INSTALL.md`](.opencode/INSTALL.md).