npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

awesome-things

v1.7.0

Published

Awesome swiss knife for Things3 – MCP server for AI, CLI, HTTP API, JS/TS API and Web

Readme

📋 awesome-things

LSK.js NPM version NPM downloads Have TypeScript types Package size Platform License Write us in Telegram

🤖 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"
    }
  }
}

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.

# 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.

| 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 }, ...]

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 📋