wrapido
v0.1.1
Published
WordPress REST API Doer: a fast CLI for any WordPress site.
Maintainers
Readme
WordPress REST API Doer: a fast CLI for any WordPress site.
A WP-CLI-style command line tool for talking to any WordPress site's REST API over HTTP — no PHP, no SSH, no WordPress install required locally. It auto-discovers a site's REST API from just a URL, introspects routes the way ?_method=OPTIONS does, and speaks WP-CLI's own --format=/--fields= conventions.
This is an independent project inspired by wp-cli/restful and the 2016 "RESTful WP-CLI" update, reimplemented as a standalone Node/TypeScript CLI that always talks to a real WordPress REST API over HTTP (rather than running inside WordPress/PHP).
Install
npm install -g wrapidoOr run it without installing: npx wrapido --help. To build from source instead, see Install from source in the full docs.
For local development, run commands directly against the TypeScript source without a build step:
npm run -s wrapido -- <namespace> <route> [<verb>] [<id>] [--flag=value...] --url=<site>Keep -s so npm's own banner stays out of the output (it would break --format=json or $(…) captures), and put wrapido's flags after -- so npm doesn't take them.
Command grammar
wrapido # discover: list namespaces from the site's REST API index
wrapido <namespace> # list routes registered under that namespace
wrapido <namespace> <route> # introspect: show the route's supported methods/args/context (an OPTIONS request)
wrapido <namespace> <route> list [--page=] [--per_page=] [...] # --per_page=-1: every page
wrapido <namespace> <route> get <id>
wrapido <namespace> <route> create [--field=value...] [--body=<json>]
wrapido <namespace> <route> update <id> [--field=value...] [--body=<json>]
wrapido <namespace> <route> delete <id> [--force]
wrapido version # print the installed wrapido version (--format=json|yaml for structured output)TLS: HTTPS certificates are never verified, so self-signed/expired certs on local or staging sites just work. Avoid real credentials on untrusted networks.
Global flags
| Flag | YAML key | Description |
| ----------------------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| --url=<site> | url | WordPress site URL. Required unless a default is saved (wrapido config set --url=) or set in a config file. |
| --username=<user> | — | Also via the WP_USERNAME env var. Not allowed in config files. |
| --password=<pass> | — | Also via the WP_PASSWORD env var. A WordPress Application Password is strongly recommended over a real account password — see Authentication. Not allowed in config files. |
| --client-id=<id> / --client-secret=<secret> | — | OAuth2 credential, for wrapido auth oauth2 login/add — see OAuth2. Not allowed in config files. |
| --token=<token> | — | OAuth2 personal access token, for wrapido auth oauth2 add. Not allowed in config files. |
| --use-auth=env\|none\|application-passwords\|oauth2 | use-auth | Pin credential resolution to one source, skipping the rest of the fallback chain — see Authentication. |
| --context=view\|edit\|embed | context | Default view. Run wrapido <namespace> <route> to see which values a given route actually supports. |
| --format=table\|json\|csv\|yaml\|ids\|count\|raw | format | Default table. Table cells are truncated per --truncate-length (a nested object/array cell shows its JSON, truncated the same way but ending in ...}/...]), unless named via --fields (e.g. --fields=title.rendered). count prints the site total from the X-WP-Total response header when WordPress sends one, else the number of rows returned. |
| --fields=<a,b,c> | — | Limit output to specific fields (table/csv also accept dotted paths like title.rendered). Per-invocation only. Also sent to the API as _fields (except on delete) to trim the response, and still filtered locally for endpoints that ignore it. |
| --field=<name> | — | Print a single field's raw value (supports dotted paths, e.g. title.rendered). Per-invocation only. |
| --body=<json> | — | Raw JSON request body for create/update, overriding/merged under --field= args. Per-invocation only. |
| --timeout=<ms> | timeout | Timeout for every HTTP request; when given it replaces all the defaults below. Defaults: 20000 (20 s) for API calls, 8000 for site discovery and wrapido auth calls, and 300000 (5 min) for file uploads/downloads, where it is an idle timeout that resets whenever data moves. |
| --no-color | color: false | Disable colored output. |
| --truncate-length=<n> | — | Max characters a table cell shows before truncating; default 50. 0 shows full values. Per-invocation only. |
| --quiet | quiet | Suppress spinners. |
| --debug | debug | Print a stack trace on unexpected (non-API) errors, and log every HTTP request/response to stderr (with the Authorization header redacted) plus the config files loaded. |
| -h, --help | — | Show help for the given namespace/route/verb, or the top-level help. |
Flags with a YAML key can be set in a config file; a flag on the command line always wins.
Any other --name=value (or bare --name, treated as --name=true) is passed straight through as a WordPress REST API field or query argument — e.g. --per_page=5, --title="Hello", --force. Run wrapido <namespace> <route> first to see exactly which ones a route accepts.
list --per_page=-1 fetches every page. The REST API itself rejects -1, so the CLI works around it the same way Gutenberg's api-fetch does: it requests the route's own maximum page size (the maximum declared in its schema, usually 100) and concatenates the pages, in parallel when X-WP-TotalPages is sent and otherwise by following Link: rel="next". This only applies to routes that declare both page and per_page. A progress bar shows while the pages load (hidden by --quiet). --page is ignored alongside it, with a warning. --format=count still makes just one request, and a per_page below -1 is rejected before anything is sent.
Using with AI agents
Auto-detected inside Claude Code, OpenAI Codex, GitHub Copilot's agent tooling, Cline and Cursor (or set WRAPIDO_AGENT=1 by hand): JSON by default (compact, _links/_embedded stripped), no colour or spinners, JSON errors, and warnings for unknown flags. Nothing changes for interactive use. See docs/agent-mode.md and AGENTS.md (written for the agent itself).
There is also a wrapido skill (skills/wrapido/) that teaches the agent the workflow, safety rules and gotchas, packaged as a plugin for several AI coding tools. It is knowledge only, so install the CLI (above) first, then:
| Tool | Install | Guide |
| --- | --- | --- |
| Claude Code | claude plugin marketplace add spacedmonkey/wrapido then claude plugin install wrapido@wrapido | Claude Code |
| GitHub Copilot CLI | copilot plugin marketplace add spacedmonkey/wrapido then copilot plugin install wrapido@wrapido | GitHub Copilot |
| VS Code (Copilot) | Add "chat.plugins.marketplaces": ["spacedmonkey/wrapido"] to settings | GitHub Copilot |
| OpenAI Codex | codex plugin marketplace add spacedmonkey/wrapido then codex plugin add wrapido@wrapido | Codex |
| Gemini CLI | gemini extensions install https://github.com/spacedmonkey/wrapido --ref=main | Gemini CLI |
| Cursor | In Customize, import "From GitHub Repository": https://github.com/spacedmonkey/wrapido | Cursor |
Export WP_USERNAME/WP_PASSWORD (an Application Password) and ask the agent to work with your site. See AI agents for the full setup and a guide for each tool.
Authentication
Use a WordPress core Application Password (Users → Profile → Application Passwords, built into WordPress since 5.6), not your real account password:
wrapido wp/v2 posts list --url=https://example.com --username=admin --password=xxxx xxxx xxxx xxxx xxxx xxxxApplication Passwords are revocable and scoped per-application, and work over the same HTTP Basic Auth the CLI always sends — nothing else about how you invoke the CLI changes if you use one. Run wrapido --url=<site> to see whether a target site supports them (reported from the REST API index).
You can also store credentials per site with wrapido auth <type> ... (<type> names the auth mechanism — today just application-passwords, matching WordPress's own key for it, leaving room for e.g. oauth2 later), instead of passing --username/--password every time — including wrapido auth application-passwords login <url>, which obtains an Application Password for you via a browser flow (no copy-pasting a password), the same way gh auth login/claude login work:
wrapido auth application-passwords login https://example.com # browser-based Application Password flow
wrapido auth application-passwords add https://example.com --username=admin --password="xxxx xxxx xxxx xxxx xxxx xxxx" # store one you already have
wrapido auth application-passwords listSee Authentication for the full wrapido auth command reference.
Auth is built behind a small AuthProvider interface so other methods (OAuth, cookie/nonce, etc.) can be added later without touching request code.
Examples
# Discover a site
wrapido --url=https://example.com
# List routes in a namespace
wrapido wp/v2 --url=https://example.com
# Introspect a route (methods, args, supported --context values)
wrapido wp/v2 posts --url=https://example.com
# List, with query args and JSON output
wrapido wp/v2 posts list --per_page=5 --format=json --url=https://example.com
# Get one, as edit context, authenticated
wrapido wp/v2 posts get 42 --context=edit --url=https://example.com --username=admin --password=xxxx-xxxx-xxxx-xxxx
# Create
wrapido wp/v2 posts create --title="Hello" --status=publish --url=https://example.com
# Update
wrapido wp/v2 posts update 42 --status=draft --url=https://example.com
# Delete
wrapido wp/v2 posts delete 42 --force --url=https://example.com
# Delete every draft at once: several ids in one command, batched through /batch/v1
wrapido --url=https://example.com wp/v2 posts delete $(wrapido --url=https://example.com wp/v2 posts list \
--per_page=-1 --fields=id --format=ids --status=draft --quiet)
# Upload a file — the flag is whatever parameter the endpoint expects (`file` for core media)
wrapido wp/v2 media create --file=./cat.jpg --title="Cat" --url=https://example.com
# Save defaults so you don't have to repeat --url/--username
wrapido config set --url=https://example.com --username=admin
wrapido config get
wrapido config clear
# Store per-site credentials instead of passing --username/--password every time
wrapido auth application-passwords login https://example.com
wrapido auth application-passwords list
wrapido auth application-passwords remove --all # if a machine/config is ever compromised
wrapido config rotate-key # re-encrypt the local store under a fresh keySee Uploading files for batches, URL imports and other endpoints.
Development
npm run -s wrapido -- <args> # run against source via tsx, no build needed (-s: no npm banner in the output)
npm run build # bundle to dist/cli.js (tsup)
npm test # vitest: unit tests (via wp-scripts) + an execa-driven integration suite against a local fixture server
npm run coverage # merged unit + integration coverage report (coverage/) and README badge update
npm run typecheck
npm run lint # wp-scripts lint-js — WordPress/Gutenberg coding standards
npm run format # wp-scripts formatCode style follows the WordPress/Gutenberg JavaScript coding standards, via @wordpress/scripts, @wordpress/eslint-plugin, and @wordpress/prettier-config.
Design notes
- Discovery:
HEADthe site → read theLink: <...>; rel="https://api.w.org/"header → fall back to the HTML<link>tag → fall back to probing/wp-json/then/?rest_route=/. - Verbs, not raw HTTP methods:
list/get/create/update/deletemirror WP-CLI's ownwp post list/wp post create/etc., but layered onto generic<namespace> <route>addressing so they work against any namespace — core or plugin — not just hardcoded resource names. - Credentials:
--username/--password(orWP_USERNAME/WP_PASSWORD) always take precedence;wrapido auth <type> login/addcan additionally store a credential per site as a fallback for requests that omit them, encrypted at rest under a random key generated once per machine and kept in its own file — protects a leaked/copied config file alone, not against something with full account-level read access (an OS keychain would be needed for that, deliberately out of scope to avoid a native-binding dependency).<type>names the auth mechanism —application-passwordstoday, matching the key WordPress's own REST API index uses for it, with the grammar (and an exhaustively-checkedAuthTypeunion internally) already shaped for a second type — e.g.oauth2— to be added later without changing the command shape again.wrapido config rotate-keyregenerates the encryption key, andwrapido auth application-passwords remove --allrevokes-where-possible and forgets every stored credential, as an incident-response pair if a machine or its config is ever suspected compromised.addverifies a credential against the site before saving (unless--skip-verify);login/removerevoke the Application Password on the site itself, not just locally, whenever the credential being replaced/removed was confirmed to be one. - Packages:
commander(parsing), nativefetch(HTTP),ora(spinner),table+flat(table rendering with dot-flattened nested fields),json-2-csv(CSV),yaml(YAML),picocolors(colors),conf+env-paths(saved defaults + per-site credentials, and locating the per-machine encryption key file),@wordpress/url(query-string building).
Contributing
Contributions are welcome. See CONTRIBUTING.md for how to get set up, the test/lint/typecheck expectations, and how to propose a change.
License
MIT — see LICENSE for the full text.
