Things3任务管理服务

awesome-things 是一个面向 macOS Things3 的集成管理服务,通过 MCP server、CLI、HTTP REST API 和 JS/TS API 提供任务、项目、标签与区域的创建、查询、完成和移动等操作,适用于 AI 客户端接入、终端自动化及自建前端。

  • by isuvorov
  • TypeScript
  • 混合部署
  • 2 星标
  • 更新于
在 GitHub 查看 isuvorov/awesome-things

使用说明

📋 awesome-things

📋 Awesome swiss knife for Things3 — MCP server, CLI, HTTP API & JS/TS API for macOS 📋

🤖 MCP server — connect ChatGPT, Claude & Cursor to manage your tasks
⚙️ CLI — automate from the terminal, cron jobs & pipelines (thi, things)
🌐 HTTP REST API — build your own frontend; OpenAPI spec + Swagger UI included
📦 JS/TS API — use Things3 as a fully-typed library
🔗 Built-in tunnels — expose your Mac via localtunnel, ngrok or frp
🔐 Bearer-token auth — protect remote access out of the box
✅ 21 operations — todos, projects, tags, areas, move, delete & batch
🍏 AppleScript-native — talks to Things3 directly, validated with Zod


Installation

# Library / programmatic use
npm install awesome-things
yarn add awesome-things
pnpm add awesome-things
bun add awesome-things
# Global CLI — exposes the `awesome-things`, `things` and `thi` binaries
npm install -g awesome-things

Or run any command on demand without installing:

npx awesome-things <command>

Basic Usage

Expose Things3 to a remote AI client (ChatGPT, Claude) in one command:

npx awesome-things server --tunnel

#  ➜  MCP: https://my-awesome-things3-mcp.loca.lt/mcp/auth/uvhSdaAsd1qmtAnHa895bcjwTAnBxw
  1. Run the command in your terminal and copy the printed MCP URL.
  2. Paste the URL into your AI client (ChatGPT, Claude, …).
  3. Ask the agent to manage your tasks.

What the AI can do once connected:

  • Create a todo "Buy groceries" in Today
  • Show all todos for today
  • Complete "Buy groceries"
  • Search for todos with "meeting"
  • Create project "Q1 Planning" in area "Work"

Usage

As an MCP server

Local (stdio) — add to your MCP client configuration:

{
  "mcpServers": {
    "things3": {
      "command": "npx -y awesome-things mcp"
    }
  }
}

Remote (HTTP over a tunnel) — point your client at the URL printed by server --tunnel:

{
  "mcpServers": {
    "things3": {
      "type": "http",
      "url": "https://my-awesome-things3-mcp.loca.lt/mcp/auth/uvhSdaAsd1qmtAnHa895bcjwTAnBxw"
    }
  }
}
Client-specific config paths

ChatGPT — Settings → Beta Features, enable "Developer Mode" and "MCP apps", then add the server in the UI (no file editing).

Claude Desktop — ~/Library/Application Support/Claude/claude_desktop_config.json

Claude Code — .claude/settings.json (project) or ~/.claude/settings.json (global)

Cursor — .cursor/mcp.json (project) or ~/.cursor/mcp.json (global)

As a CLI

Install globally (npm i -g awesome-things) to avoid the npx startup delay. thi and things are aliases for awesome-things.

thi list today
thi add "Buy milk" --list today
thi done "Buy cheese"
thi search "report"
thi info                # which build is running, and where it came from

Global flags: --json for machine-readable output, --format pretty|table|plain (-f) for the rendering style.

All CLI commands
# Create a todo
things add "Buy milk"
things add "Submit report" --notes "Q1" --due 2026-03-01 --tags work urgent --list today
things add "Sand the stool" --when 2026-09-12          # Things' "When", not the deadline
things add "Call the bank" --when tomorrow
things add "Standup" --when 2026-09-12@11:00           # with a time = phone notification
things add "Fix leak" --area Home
things add "Plan sprint" --project "Q1 Planning"

# List todos (inbox | today | anytime | upcoming | someday | logbook)
things list              # today (default)
things list inbox
things list today --status open

# Finish a todo — three different things (by name or --id)
things done "Buy milk"                  # done      → Logbook, as completed
things cancel "Buy milk"                # cancelled → Logbook, will not happen
things rm "Buy milk"                    # deleted   → Trash (alias: things trash)

# Batch: the same operation over several ids in one go
things rm --ids ABC123 DEF456 GHI789
things done --ids ABC123 DEF456

# Update a todo
things update "Submit report" --new-name "Submit Q1 report"
things update "Submit report" --new-due 2026-03-15
things update "Submit report" --new-due none      # clear the deadline
things update "Submit report" --new-when 2026-09-12       # YYYY-MM-DD[@HH:MM] | today | tomorrow | evening
things update "Submit report" --new-when none            # clear the When date
things update --ids ABC123 DEF456 --new-tags work        # same update, several todos

# Checklists (need AWESOME_THINGS_URL_TOKEN)
things add "Pack" --checklist Passport Tickets Charger
things update "Pack" --new-checklist Passport Tickets

# Search
things search "report"

# Projects
things project add "Q1 Planning" --area Work
things project add "Trip" --todo "Book flight" --todo "Pack"   # project + its todos in one call
things project update "Q1 Planning" --new-notes "See https://example.com/roadmap"
things project list
things project todos "Q1 Planning"
things project rm "Q1 Planning"          # project + its todos → Trash

# Tags & areas
things tags
things areas
things area Work                         # todos sitting directly in an area (no project)

# Move (destination: inbox | today | evening | anytime | upcoming | someday)
things move todo "Buy milk" today
things move todo-to-project "Submit report" "Q1 Planning"
things move todo-to-area "Submit report" Work
things move project-to-area "Q1 Planning" Work

# Remove associations
things remove todo-from-project "Submit report"
things remove project-from-area "Q1 Planning"

# Environment info — version, which binary is actually running, where it came from
things info
things info --json

As an HTTP REST API

things server                      # default port 32123, random Bearer token printed on startup
things server --no-token           # disable auth (local use)
things server --port 8080          # custom port

The server also serves a home page at /, a health check at /health, Swagger UI at /api, and the raw OpenAPI spec at /api/openapi.json.

Endpoints
Method Path Description
GET /api/todos?list=today&status=open List todos
POST /api/todos Create todo
PUT /api/todos Update todo
POST /api/todos/complete Complete todo
POST /api/todos/cancel Cancel todo
POST /api/todos/delete Delete todo (→ Trash)
GET /api/todos/search?q=... Search todos
GET /api/projects?area=... List projects
POST /api/projects Create project
POST /api/projects/delete Delete project (→ Trash)
GET /api/projects/:name/todos Project todos
GET /api/tags All tags
GET /api/areas All areas
GET /api/areas/:name/todos Todos directly in an area
POST /api/move/todo Move todo to list
POST /api/move/todo-to-project Move todo to project
POST /api/move/todo-to-area Move todo to area
POST /api/move/project-to-area Move project to area
POST /api/remove/todo-from-project Remove from project
POST /api/remove/project-from-area Remove from area
GET /mcp, /mcp/auth/{token} MCP-over-HTTP transport
curl http://localhost:32123/api/todos?list=today

curl -X POST http://localhost:32123/api/todos \
  -H "Content-Type: application/json" \
  -d '{"name": "Buy milk", "list": "today"}'

curl "http://localhost:32123/api/todos/search?q=report"

# When auth is enabled, pass the printed token:
curl http://localhost:32123/api/tags -H "Authorization: Bearer <token>"

As a background daemon

things server dies with its terminal. things daemon installs the same server as a launchd user agent: it starts at login, restarts itself after a crash, and writes both streams to log files you can tail at any time.

things daemon install              # write the agent, start it, print URL + token
things daemon install --port 8080 --tunnel frp --domain things.example.com
things daemon install --no-start   # only write the plist
things daemon status               # running? healthy? which pid, which token
things daemon restart              # pick up a new build
things daemon stop                 # stop without uninstalling
things daemon uninstall            # stop and remove the agent (--purge also drops the logs)

The log holds events, not the startup banner: one started line (version, pid, URL, auth, config file), tunnel when one opens, stopped on SIGTERM, then a line per request. The token and the MCP client configs are only printed to a terminal.

Watching it work — the daemon has no terminal, so the logs are the way in:

things daemon logs                 # last 50 lines of both streams
things daemon logs -f              # follow live (Ctrl+C to stop)
things daemon logs -f --err        # only errors
things daemon logs -n 200 --out    # last 200 request lines
things daemon logs --clear         # truncate both files
What Where
Agent ~/Library/LaunchAgents/com.isuvorov.awesome-things.plist (mode 600)
Requests ~/.local/share/awesome-things/logs/server.log
Errors ~/.local/share/awesome-things/logs/server.error.log
Health curl http://localhost:32123/health

Notes:

  • The bearer token is pinned into the agent at install time (taken from --token, AWESOME_THINGS_TOKEN, the previously installed agent, or generated). A daemonized server cannot print a fresh random token anywhere you would see it.
  • install is safe to repeat: with the same flags and environment the plist comes out identical and the running daemon is left alone. Only a real change restarts it.
  • launchd starts a job with an almost empty environment. install bakes in PATH, HOME and every AWESOME_THINGS_* / FRP_* / NGROK_AUTHTOKEN variable from the shell you ran it in — change one of them and run things daemon install again.
  • Automation is granted to awesome-things, not to bun or node. The daemon runs through a tiny launcher app (~/.local/share/awesome-things/awesome-things.app, built and signed on install), so macOS asks "awesome-things wants to control Things3" and the permission covers this app only — never every script on your runtime. Allow it on the first Things3 call; it is listed under System Settings → Privacy & Security → Automation. Building it needs the Command Line Tools (xcode-select --install); --no-launcher skips it.
  • The icon in Login Items needs a real certificate. macOS takes a background item's name and icon from its code signature, and an ad-hoc signature has none of that — the item shows as a generic "exec". If your keychain has a Developer ID Application or Apple Development certificate (the latter is free: sign in to Xcode with your Apple ID), install signs the launcher with it automatically. Pick one with AWESOME_THINGS_SIGN_IDENTITY (SHA-1 or part of the name), or - for ad-hoc.
  • Logs are rotated to .1 on install/restart once they pass 10 MB — launchd rotates nothing itself.
  • macOS only; on other platforms the command refuses instead of pretending.

As a JS/TS library

import { createTodo, listTodos, completeTodo } from 'awesome-things';

// Create a todo
await createTodo({ name: 'Buy milk', list: 'today' });

// List today's open todos
const todos = await listTodos({ list: 'today', status: 'open' });

// Complete a todo
await completeTodo({ name: 'Buy milk' });

Configuration

Tunneling

Built-in tunnel support exposes the local server for remote access — useful for connecting AI clients (ChatGPT, Claude) to Things3 on your Mac. Three providers are supported.

# localtunnel (default) — free, no signup, no config
things server --tunnel
things server --tunnel --domain myapp                     # custom subdomain

# ngrok — needs an auth token
things server --tunnel=ngrok --ngrok-token=YOUR_TOKEN
NGROK_AUTHTOKEN=YOUR_TOKEN things server --tunnel=ngrok
things server --tunnel=ngrok --domain myapp.ngrok-free.app

# frp — self-hosted reverse proxy; needs the `frpc` binary + your own frp server
FRP_SERVER_ADDR=frp.example.com things server --tunnel=frp
FRP_SERVER_ADDR=frp.example.com things server --tunnel=frp --domain myapp

Config file

Settings live in ~/.config/awesome-things/config.json (or $XDG_CONFIG_HOME/awesome-things/, or any path in AWESOME_THINGS_CONFIG). Every key mirrors an environment variable below:

{
  "$schema": "https://unpkg.com/awesome-things/config.schema.json",
  "port": 32123,
  "token": "your-bearer-token",
  "urlToken": "things-url-scheme-token",
  "tunnel": "frp",
  "domain": "things.example.com",
  "frp": { "serverAddr": "frp.example.com", "serverPort": 7000, "token": "frp-token" },
  "ngrok": { "authtoken": "ngrok-token" }
}
  • Comments: JSON has none, so any key starting with _ or // is ignored at every level — "_frp": {...} switches a section off, "_note": "..." explains a value. Any other unknown key is a typo and an error.
  • Precedence: CLI flag > environment variable > config.json > default.
  • $schema gives your editor autocompletion and validation; the schema ships in the npm package.
  • An invalid file stops the process with the exact field at fault — a server never starts with half a config. Keep it chmod 600: it holds tokens, and a looser mode prints a warning.
  • The daemon rereads the file on every start, so after editing it things daemon restart is enough. Values that come from the file are never frozen into the launchd plist.

Environment variables

Variable Purpose Default
AWESOME_THINGS_PORT / PORT HTTP server port 32123
AWESOME_THINGS_TOKEN Bearer token for the HTTP/MCP API random per start
AWESOME_THINGS_TUNNEL Tunnel provider (localtunnel, ngrok, frp) —
AWESOME_THINGS_DOMAIN Tunnel domain / subdomain —
AWESOME_THINGS_URL_TOKEN Things URL-scheme token — needed for evening, reminder times (--when 2026-09-12@11:00) and checklist items. Things → Settings → General —
NGROK_AUTHTOKEN ngrok auth token —
FRP_SERVER_ADDR frp server address (required for frp) —
FRP_SERVER_PORT frp server port 7000
FRP_TOKEN frp authentication token —
FRP_PROTOCOL frp protocol (http or https) https
FRP_SUBDOMAIN frp subdomain —
FRP_REMOTE_PORT frp remote port mapping —
FRP_PROXY_NAME proxy name in frpc things

All FRP_* variables also accept the AWESOME_THINGS_FRP_* prefix.


How it works

AppleScript is the only programmatic interface to Things3 on macOS. Each operation is a typed function that builds an AppleScript snippet, runs it via osascript, and parses the result. The four interfaces are thin layers over the same set of operations.

Module Responsibility
src/api.ts Public JS/TS API — re-exports all functions and types
src/mcp.ts MCP server (stdio transport)
src/server.ts HTTP REST API + MCP-over-HTTP (Bun.serve)
src/cli.ts CLI (yargs) — things / thi / awesome-things
src/api/*.ts The 21 todo / project / area / list / move operations
src/utils/applescript.ts execute, tellThings, quoteString helpers
src/utils/tunnel.ts localtunnel / ngrok / frp providers
src/utils/auth.ts Bearer-token resolution & checks

All operations identify todos and projects by name or by id, and every input is validated with a Zod schema. Todo operations also accept ids: string[] and then run as a batch.

Done, cancelled and deleted are three different things. completeTodo puts a todo in the Logbook as done — do not use it to get rid of a todo you created by mistake, or the Logbook ends up full of work nobody did. Use cancelTodo when the work will not happen, and deleteTodo / deleteProject (which move the item to the Trash) when it should never have existed.

What Things3 itself cannot do: recurring todos (repeat) and headings inside a project are exposed by neither AppleScript nor the URL scheme, so this package cannot set them — Repeat has to be set by hand in Things, and headings need a things:///json payload.

macOS permissions

Things3 must be running, and the app that launched the process (Terminal, your editor, an agent runtime) needs Automation access to Things3 in System Settings → Privacy & Security → Automation. Without it macOS withholds the scripting dictionary and every call fails with -2740 / -2741 / -1743 — the package detects that and says so instead of forwarding raw AppleScript noise. Sandboxed environments without Apple Events access cannot reach Things3 at all.


API Reference

Core functions

import {
  createTodo,
  searchTodos,
  updateTodo,
  createProject,
  moveTodoToProject,
  deleteTodo,
} from 'awesome-things';

// Create with all options
await createTodo({
  name: 'Submit report',
  notes: 'Q1 financials',
  due_date: '2026-03-01',
  tags: ['work', 'urgent'],
  list: 'today',
});

// Create a todo directly in an area
await createTodo({ name: 'Fix leak', area: 'Home' });

// Search across all lists
const results = await searchTodos({ query: 'report' });

// Update a todo
await updateTodo({ name: 'Submit report', new_due_date: '2026-03-15' });

// Create a project together with its todos
await createProject({
  name: 'Q1 Planning',
  area: 'Work',
  todos: ['Draft the roadmap', { name: 'Review with the team', when: 'tomorrow' }],
});
await moveTodoToProject({ todo_name: 'Submit report', project_name: 'Q1 Planning' });

// Trash seven todos in one call — and see which ones failed
const result = await deleteTodo({ ids: ['ABC', 'DEF', 'GHI'] });
result.ids;      // the ones that worked
result.results;  // [{ id, ok, message }, ...]
All 21 functions

Every todo function accepts ids: string[] instead of id to run as a batch.

  • createTodo({ name, notes?, due_date?, when?, tags?, list?, project?, area?, checklist? }) — create a todo
  • listTodos({ list, status? }) — list todos
  • completeTodo({ name?, id?, ids? }) — mark as done (→ Logbook)
  • cancelTodo({ name?, id?, ids? }) — mark as cancelled (→ Logbook)
  • deleteTodo({ name?, id?, ids? }) — delete (→ Trash)
  • updateTodo({ name?, id?, ids?, new_name?, new_notes?, new_due_date?, new_when?, new_tags?, new_checklist? }) — update a todo
  • searchTodos({ query }) — search todos by name
  • createProject({ name, notes?, area?, todos? }) — create a project, optionally with its todos
  • updateProject({ project_name, new_name?, new_notes?, new_due_date?, new_tags?, new_area? }) — update a project
  • deleteProject({ project_name }) — delete a project and its todos (→ Trash)
  • listProjects({ area? }) — list projects
  • getProjectTodos({ project_name, status? }) — get project todos
  • getAreaTodos({ area_name, status? }) — todos sitting directly in an area
  • listTags() — list all tags
  • listAreas() — list all areas
  • moveTodo({ todo_name?, id?, ids?, destination }) — move todo to a list
  • moveTodoToProject({ todo_name?, id?, ids?, project_name }) — move todo to a project
  • moveTodoToArea({ todo_name?, id?, ids?, area_name }) — move todo to an area
  • moveProjectToArea({ project_name, area_name }) — move project to an area
  • removeTodoFromProject({ todo_name?, id?, ids? }) — remove todo from its project
  • removeProjectFromArea({ project_name }) — remove project from its area

Subpath imports

import { createTodo } from 'awesome-things';        // main aggregator
import { createTodo } from 'awesome-things/api';    // direct API
import 'awesome-things/mcp';                         // MCP server
import 'awesome-things/server';                      // HTTP server
import 'awesome-things/cli';                         // CLI

Custom AppleScript

import { execute, tellThings } from 'awesome-things';

const result = await execute(
  tellThings('show quick entry panel with properties {name:"Hello"}'),
);

Examples

Add a todo for every .log file in the current directory:

for f in *.log; do thi add "Review $f" --list today; done

List today's open todos as JSON and pipe into jq:

thi list today --status open --json | jq '.todos[].name'

Tests

bun run test          # lint + types + unit tests + size-limit
bun run test:unit     # unit tests only (bun:test)

Unit tests cover the pure functions (AppleScript helpers, Zod schemas) and the HTTP server. Integration tests require macOS with Things3 running.


Development

bun install
bun run build       # build (tsdown -> lib/)
bun run dev         # watch mode
bun run start       # MCP server (stdio)
bun run server      # HTTP API server
bun run cli         # run the CLI
bun run fix         # auto-fix lint & formatting (biome)

Limitations

  • macOS only — Things3 has no API other than AppleScript.
  • Things3 must be installed and running.
  • Operations identify items by name (or id); duplicate names act on the first match.

License

MIT © Igor Suvorov — see LICENSE.


awesome-things — Manage Things3 from anywhere 📋