diff --git a/README.md b/README.md
index 7b9532d..d9251a5 100644
--- a/README.md
+++ b/README.md
@@ -1,453 +1,156 @@
-# MainWP Control
-
-*A [MainWP Labs](https://mainwp.com/mainwp-labs/) project, powered by MainWP*
-
-A CLI for managing your MainWP Dashboard from the terminal. List sites, push updates, sync data, run batch operations across dozens of sites. The command is `mainwpcontrol`.
-
-**Looking for the MCP Server instead?** [MainWP MCP Server](https://github.com/mainwp/mainwp-mcp) is for conversational AI management inside Claude, Cursor, or any MCP-compatible client. MainWP Control is for automation: cron jobs, CI/CD pipelines, monitoring scripts, and batch operations. Both talk to the same Abilities API with the same safety model.
+
+
+
----
+
+
+
+
+
-## Quick Start
+# MainWP Control
-> **On Windows?** Use [Git Bash](https://gitforwindows.org/) and every example below works without changes. For scheduled workflows (cron), see [WSL](https://learn.microsoft.com/en-us/windows/wsl/install).
+_A [MainWP Labs](https://mainwp.com/mainwp-labs/) project, powered by MainWP_
-You need Node.js 20.18.1+ and a MainWP Dashboard (v6+) with an [Application Password](https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/).
+Manage your whole WordPress network from the terminal. [MainWP Control](https://github.com/mainwp/mainwp-control) is a command-line interface for your MainWP Dashboard, built for the work you do on a schedule rather than in a browser tab:
```bash
-npm install -g @mainwp/control
-
-mainwpcontrol login
-
-mainwpcontrol abilities list
-```
-
-You should see something like this:
+# Which sites have pending updates?
+mainwpcontrol abilities run list-updates-v1 --json
-```text
-Abilities (87 total)
+# Sync every site and wait for the result
+mainwpcontrol abilities run sync-sites-v1 --wait --json
- Sites
-Name Description Type
----------------- -------------------- --------------
-list-sites-v1 List MainWP sites 📖 read
- mainwpcontrol abilities run list-sites-v1
-get-site-v1 Get site details 📖 read
- mainwpcontrol abilities run get-site-v1
-sync-sites-v1 Sync all sites ✏️ write
- mainwpcontrol abilities run sync-sites-v1
+# Nightly health check from cron, alert on failure
+mainwpcontrol abilities run check-sites-v1 --quiet \
+ || curl -fsS "$SLACK_WEBHOOK" -d '{"text":"Sites down"}'
```
-That's it. You're connected and you can see every operation your Dashboard supports.
-
----
-
-## What Just Happened
+The CLI is a small program that runs on your own computer or server. Nothing new is installed on your Dashboard or your child sites; it talks to the same Abilities API your Dashboard already exposes. Anything classified as destructive stops for a preview and your explicit confirmation before it runs.
-`abilities list` shows every operation available on your Dashboard. These are called "abilities" and they cover sites, plugins, themes, updates, clients, tags, and more.
+
+
+
-Each ability has a name (like `list-sites-v1`) that you pass to `abilities run` to execute it. The list tells you whether each one is read-only, a write operation, or destructive.
+**Looking for the MCP Server instead?** [MainWP MCP Server](https://github.com/mainwp/mainwp-mcp) connects Claude, Cursor, and other AI tools to your Dashboard for conversational management. MainWP Control is for automation: cron jobs, CI/CD pipelines, monitoring scripts, and batch operations. Both talk to the same Abilities API with the same safety model.
----
-
-## Common Use Cases
-
-**List all your sites:**
-
-```bash
-mainwpcontrol abilities run list-sites-v1 --json
-```
-
-**Check for pending updates across sites:**
+## Why a CLI?
-```bash
-mainwpcontrol abilities run list-updates-v1 --json
-```
-
-**Get details for a specific site:**
-
-```bash
-mainwpcontrol abilities run get-site-v1 --input '{"site_id_or_domain": 1}' --json
-```
+The Dashboard UI is built for a person clicking through sites. A CLI is built for everything that should happen without you watching:
-> **Windows?** This works as-is in [Git Bash](https://gitforwindows.org/). In PowerShell, use `--input-file` instead of inline JSON: how PowerShell passes quoted arguments to native commands varies by version ([details](docs/workflows/input-from-file.md)).
+- **Scriptable output.** `--json` prints exactly one machine-readable envelope on stdout. Warnings and progress never contaminate it, so you can pipe results straight into `jq`, a spreadsheet, or a monitoring agent.
+- **Honest exit codes.** Classified outcomes exit 0-5 by error class (input, auth, network, API, internal); interruptions use the standard signal exits (130/143). Your pipeline branches on the code instead of parsing error text.
+- **Batch operations that survive.** Large operations queue as batch jobs with a `job_id` you can watch, wait on, or come back to. A timeout or Ctrl-C leaves you with a resumable job, not a mystery.
+- **Credentials that stay put.** Passwords live in your OS keychain, or in an environment variable for CI. The profile file on disk never contains them.
+- **Guard rails you can't script around by accident.** Destructive abilities require a successful `--dry-run` preview and an explicit `--confirm`, and each confirmed run is written to a local audit log.
-**Preview a destructive action before running it:**
+## What You Can Do
-```bash
-mainwpcontrol abilities run delete-site-v1 \
- --input '{"site_id_or_domain": "mysite.com"}' \
- --dry-run --json
-```
+- **Site Management**: List sites, check connection status, sync data, add or remove child sites
+- **Update Management**: See pending updates across all sites, apply core/plugin/theme updates
+- **Plugin and Theme Control**: View installed plugins and themes, activate or deactivate them
+- **Client Organization**: Manage client records, assign sites to clients, track costs
+- **Bulk Operations**: Sync, reconnect, or check connectivity across dozens of sites at once
+- **Chat Mode**: Optionally talk to your Dashboard in plain English, with the same safety gates
-Nothing changes until you explicitly pass `--confirm`.
+Built for WordPress agencies and site managers who want their MainWP routine in scripts, schedulers, and pipelines.
-**Update plugins and wait for completion:**
+## Quick Start
-```bash
-mainwpcontrol abilities run update-site-plugins-v1 \
- --input '{"site_id": 1}' \
- --wait --json
-```
+**Requirements:** Node.js >=20.18.1 and MainWP Dashboard 6.0+
-`--wait` blocks until the operation finishes. Useful in CI pipelines.
+> **On Windows?** Use [Git Bash](https://gitforwindows.org/) and every example below works without changes. For scheduled workflows (cron), see [WSL](https://learn.microsoft.com/en-us/windows/wsl/install).
----
+**1. Create an Application Password.** This is a separate password WordPress issues for tools like this one; it never changes your login and you can revoke it at any time.
-## Installation
+1. Log into your MainWP Dashboard as an administrator
+2. Go to **Users > Profile** (click your username in the top right)
+3. Scroll to the **Application Passwords** section
+4. Enter a name like "MainWP Control" and click **Add New Application Password**
+5. Copy the generated password immediately (it is only shown once; spaces are fine either way)
-### Standard install (recommended)
+> **Tip:** Create a dedicated WordPress user for API access rather than using your main admin account. It keeps the audit trail clean and is easy to revoke later.
-Pre-built keychain binaries are included for macOS, Windows, and Linux (x64 and arm64). On other platforms you may need C++ build tools during installation.
+**2. Install and log in.**
```bash
npm install -g @mainwp/control
-# Interactive login (stores credentials in your OS keychain)
mainwpcontrol login
```
-### Environment variable auth (CI, Docker, headless)
-
-Use this when no OS keychain is available, or if keytar fails to build.
-
-```bash
-npm install -g @mainwp/control
-
-export MAINWP_APP_PASSWORD='xxxx xxxx xxxx xxxx xxxx xxxx'
-
-mainwpcontrol login --url https://dashboard.example.com --username admin
-```
-
-When the OS keychain is unavailable, credentials are not stored on disk. Keep `MAINWP_APP_PASSWORD` set for each run.
-
-If keytar is installed but broken, set `MAINWPCONTROL_NO_KEYTAR=1` to skip loading it.
-
----
-
-
-New to the Command Line?
-
-If you haven't used a terminal before, here's what you need to know.
-
-### What is a terminal?
-
-A terminal is where you type commands instead of clicking buttons. You'll see it called "command line" or "shell" in different places.
-
-**How to open it:**
-- **macOS**: Open **Terminal** (search in Spotlight, or look in Applications > Utilities)
-- **Windows**: Open **Git Bash** (installed with [Git for Windows](https://gitforwindows.org/)). If you don't have it, PowerShell works too; see the [quoting notes](#json-quoting-on-the-command-line) below.
-- **Linux**: Open your distribution's **Terminal** app (usually in the applications menu)
-
-### What does `npm install -g` do?
-
-`npm` is the Node.js package manager. It downloads and installs JavaScript packages. The `-g` flag installs globally, which makes `mainwpcontrol` available as a command anywhere on your system, not only in one project folder.
-
-### What is an environment variable?
-
-An environment variable is a named value that programs can read. They're commonly used for passwords and API keys.
-
-**Setting one:**
-```bash
-# macOS / Linux (lasts until you close the terminal)
-export MAINWP_APP_PASSWORD='xxxx xxxx xxxx xxxx xxxx xxxx'
-
-# Windows PowerShell (lasts until you close the window)
-$env:MAINWP_APP_PASSWORD = 'xxxx xxxx xxxx xxxx xxxx xxxx'
-```
-
-For long-term storage, use the OS keychain (the default when you run `mainwpcontrol login`) or a restricted-permission `.env` file rather than pasting credentials into shell profile files.
-
-### What is an Application Password?
-
-WordPress Application Passwords let external tools like `mainwpcontrol` access your site without using your main login password. They look like groups of four characters separated by spaces (e.g., `abcd efgh ijkl mnop qrst uvwx`).
-
-**To create one:** Log into WordPress admin > Users > Your Profile > scroll to **Application Passwords** > enter a name like "mainwpcontrol" > click **Add New Application Password** > copy the generated password.
-
-### Reading command output
-
-When you run a command, the output appears in your terminal. A few things to know:
-
-- **`--json`** tells `mainwpcontrol` to output structured JSON (useful for scripting and piping to other tools)
-- **Exit codes** indicate success (`0`) or failure (`1` through `5`). You won't see them directly, but scripts and CI use them to decide what happens next. Run `echo $?` (macOS/Linux) or `echo $LASTEXITCODE` (PowerShell) after a command to check.
-
-### JSON quoting on the command line
-
-When you pass JSON with `--input`, quoting depends on your shell:
-
-```bash
-# macOS / Linux / Git Bash on Windows
-mainwpcontrol abilities run get-site-v1 --input '{"site_id_or_domain": 1}' --json
-```
-
-**Git Bash on Windows** (comes with [Git for Windows](https://gitforwindows.org/)) handles quoting the same way macOS and Linux do. If you use Git Bash, all the examples in this documentation work without changes.
-
-**Windows PowerShell** quoting of inline JSON is unreliable: whether backslash-escaped quotes inside a single-quoted string reach the command intact depends on your PowerShell version. Don't fight it, put your parameters in a file and use `--input-file`:
-
-```bash
-mainwpcontrol abilities run get-site-v1 --input-file params.json --json
-```
-
-This works the same on every platform. See [Input from File](docs/workflows/input-from-file.md) for details.
-
-
-
----
-
-## Basic Usage
+`login` prompts for your Dashboard URL, username, and the Application Password, then stores the credentials in your OS keychain. For CI and headless machines, use [environment variable auth](docs/configuration.md#environment-variable-auth) instead.
-### Abilities
-
-Your Dashboard exposes its operations as "abilities." You browse them, pick one, and run it. Every ability has a versioned name like `list-sites-v1` that you pass to `abilities run`.
+**3. See what your Dashboard can do.**
```bash
-# List all abilities
mainwpcontrol abilities list
-
-# Filter by category
-mainwpcontrol abilities list --category sites
-
-# Get full details and input schema for an ability
-mainwpcontrol abilities info list-sites-v1
-
-# Run an ability
-mainwpcontrol abilities run list-sites-v1 --json
-
-# Run with input parameters
-mainwpcontrol abilities run get-site-v1 --input '{"site_id_or_domain": 1}' --json
-
-# Or use a file (works everywhere, and is the reliable option on Windows PowerShell)
-mainwpcontrol abilities run get-site-v1 --input-file params.json --json
-```
-
-### Profiles
-
-Each `mainwpcontrol login` creates a profile, a named connection to a Dashboard, identified by hostname. If you manage multiple Dashboards, run `login` once per Dashboard to create a profile for each.
-
-```bash
-# List all profiles
-mainwpcontrol profile list
-
-# Switch active profile
-mainwpcontrol profile use production.example.com
-
-# Use a profile for one command without switching
-mainwpcontrol abilities list --profile staging.example.com
-
-# Delete a profile and its keychain credentials
-mainwpcontrol profile delete staging.example.com
```
-### Diagnostics
-
-`doctor` checks your configuration, credentials, and Dashboard connectivity. Run it first if something isn't working.
-
-```bash
-# Check configuration and connectivity
-mainwpcontrol doctor
-
-# Verbose output
-mainwpcontrol doctor -v
+```text
+Abilities (87 total)
-# JSON output
-mainwpcontrol doctor --json
+ Sites
+Name Description Type
+---------------- -------------------- --------------
+list-sites-v1 List MainWP sites 📖 read
+ mainwpcontrol abilities run list-sites-v1
+get-site-v1 Get site details 📖 read
+ mainwpcontrol abilities run get-site-v1
+sync-sites-v1 Sync all sites ✏️ write
+ mainwpcontrol abilities run sync-sites-v1
```
-### Chat Mode
-
-If you have an LLM API key, you can talk to your Dashboard in plain English instead of constructing commands. Good for exploration, not required for anything.
-
-Set one of these environment variables to enable it:
+Every operation your Dashboard supports, with the exact command to run it and whether it reads, writes, or destroys. Pick one and run it:
```bash
-# Pick one (Anthropic, OpenAI, Google, or OpenRouter)
-export ANTHROPIC_API_KEY='sk-ant-...'
-
-mainwpcontrol chat
-mainwpcontrol chat "list all sites with pending updates"
+mainwpcontrol abilities run list-sites-v1 --json
```
-See [Chat Mode Configuration](#chat-mode-configuration) for all supported providers and flags.
-
-### Global Flags
-
-These flags work on every `mainwpcontrol` command except the built-in `help` and `autocomplete` commands.
-
-| Flag | Description |
-|------|-------------|
-| `--json` | Structured JSON output |
-| `--quiet` / `-q` | Suppress output (exit code only) |
-| `--profile ` | Use a specific profile |
-| `--debug` | Show redacted debug diagnostics on stderr |
-| `--help` | Show help |
-
-### Abilities Run Flags
-
-Extra flags for `abilities run`. These control input, safety checks, and batch job behavior.
-
-| Flag | Description |
-|------|-------------|
-| `--input` / `-i` | Input parameters as JSON (use `-` for stdin) |
-| `--input-file` | Read input from a JSON file |
-| `--dry-run` | Preview changes without executing |
-| `--confirm` | Execute a destructive ability |
-| `--force` | Skip interactive confirmation (CI mode) |
-| `--wait` | Block until batch job completes |
-| `--wait-timeout` | Max seconds to wait (default: 300) |
-
----
-
-## Concepts
-
-### Abilities
-
-Abilities are the operations your MainWP Dashboard exposes through its REST API. Each one has:
-
-- A versioned name (e.g., `list-sites-v1`, `delete-site-v1`)
-- An input schema (what parameters it accepts)
-- Annotations that tell you what kind of operation it is
-
-The annotations matter:
-- **Readonly**: Safe to run anytime. Cannot modify data.
-- **Destructive**: Permanently changes or deletes data. Requires `--dry-run` preview, then `--confirm` to execute.
-- **Idempotent**: Safe to re-run. Same result on repeated calls.
-
-Run `mainwpcontrol abilities info ` to see the full schema and annotations for any ability.
-
-### Profiles
+> **New to the command line?** [Getting Started](docs/getting-started.md) covers terminals, environment variables, JSON quoting, and everything else the other guides assume.
-A profile is a named connection to a MainWP Dashboard. It stores the Dashboard URL and username. Your password stays in the OS keychain (or in the `MAINWP_APP_PASSWORD` environment variable when no keychain is available).
-
-Running `mainwpcontrol login` creates a profile automatically, named after the Dashboard hostname:
+## Everyday Commands
```bash
-# Creates profile "staging.example.com"
-mainwpcontrol login --url https://staging.example.com --username admin
-
-# Creates profile "production.example.com"
-mainwpcontrol login --url https://production.example.com --username admin
-```
-
-The profile file at `~/.config/mainwpcontrol/profiles.json` never contains passwords.
-
-### Safety Model
+# Check for pending updates across all sites
+mainwpcontrol abilities run list-updates-v1 --json
-Destructive operations follow a two-step pattern: preview first, then execute.
+# Get details for one site
+mainwpcontrol abilities run get-site-v1 --input '{"site_id_or_domain": 1}' --json
-```bash
-# Step 1: Preview (nothing changes)
+# Preview a destructive action; nothing changes without --confirm
mainwpcontrol abilities run delete-site-v1 \
--input '{"site_id_or_domain": "mysite.com"}' \
--dry-run --json
-# Step 2: Execute after reviewing the preview
-mainwpcontrol abilities run delete-site-v1 \
- --input '{"site_id_or_domain": "mysite.com"}' \
- --confirm --force --json
-```
-
-`--dry-run` and `--confirm` are mutually exclusive. You cannot pass both.
-
-In CI/scripted workflows where you've already validated the operation, pass `--confirm --force` directly to skip the interactive prompt.
-
-### Batch Jobs
-
-Operations that affect many items (200+) are automatically queued as batch jobs. The command returns a `job_id` immediately, and you can watch progress:
-
-```bash
-mainwpcontrol jobs watch
-
-# With a timeout
-mainwpcontrol jobs watch --timeout 120
-```
-
-Or use `--wait` on the original command to block until completion:
-
-```bash
-mainwpcontrol abilities run sync-sites-v1 --wait --wait-timeout 300 --json
-```
-
----
-
-## Advanced Usage
-
-### CI/CD Patterns
-
-```bash
-# Non-interactive login
-export MAINWP_APP_PASSWORD='xxxx xxxx xxxx xxxx xxxx xxxx'
-mainwpcontrol login --url https://dashboard.example.com --username admin
-
-# Silent execution with exit codes
-mainwpcontrol abilities run list-sites-v1 --json --quiet
-echo "Exit code: $?"
-
-# Pipeline branching on exit codes
-if mainwpcontrol abilities run check-sites-v1 --json --quiet; then
- echo "All sites healthy"
-else
- echo "Issues detected"
-fi
-```
-
-### Input from Files and Stdin
-
-```bash
-# From a JSON file
-mainwpcontrol abilities run update-site-plugins-v1 --input-file params.json --json
-
-# From stdin
-echo '{"site_id_or_domain": 1}' | mainwpcontrol abilities run get-site-v1 --input - --json
-
-# Heredoc
-mainwpcontrol abilities run get-site-v1 --input - --json <` or per-command with `--profile `. See the [CLI Reference](docs/cli-reference.md#profile) for details.
-Settings live at `~/.config/mainwpcontrol/settings.json`:
+> **Windows PowerShell** quoting of inline JSON is unreliable; put parameters in a file and use `--input-file` instead. Git Bash handles the examples as written. Details in [Input from File](docs/workflows/input-from-file.md).
-```json
-{
- "defaultJsonOutput": true,
- "timeout": 30000,
- "debug": false,
- "llmProvider": "openai",
- "chatContextMessages": 20
-}
-```
+## Documentation
-### Workflow Guides
+- [Getting Started](docs/getting-started.md): terminals, npm, Application Passwords, and JSON quoting, if the command line is new territory
+- [CLI Reference](docs/cli-reference.md): every command, flag, and exit code
+- [Configuration](docs/configuration.md): profiles, settings.json, environment variables, and credential storage
+- [Safety & Destructive Operations](docs/safety.md): the preview-and-confirm flow, ability annotations, batch jobs, and what the guard rails do and don't cover
+- [Chat Mode](docs/chat.md): plain-English management with your own LLM API key
+- [Troubleshooting](docs/troubleshooting.md): install failures, connection errors, and authentication issues
-Step-by-step guides for common automation patterns:
+Step-by-step automation guides live in [docs/workflows](docs/workflows):
| Workflow | Description |
|----------|-------------|
@@ -457,127 +160,53 @@ Step-by-step guides for common automation patterns:
| [Input from File](docs/workflows/input-from-file.md) | Pass complex parameters via JSON files, stdin pipes, or heredocs |
| [Monitoring Integration](docs/workflows/monitoring-integration.md) | Send site metrics to Datadog, StatsD, or other monitoring tools |
----
-
-## Reference
-
-### Exit Codes
-
-| Code | Meaning | CI Usage |
-|------|---------|----------|
-| 0 | Success | Continue pipeline |
-| 1 | User/input error | Fix command syntax |
-| 2 | Auth/config error | Check credentials |
-| 3 | Network error | Retry or check connectivity |
-| 4 | API error | Check ability parameters |
-| 5 | Internal error | Report bug |
-| 130 | Interrupted (SIGINT) | Ctrl-C during a prompt or `jobs watch`; standard Unix 128+SIGINT convention, outside the 0-5 contract |
-
-### Environment Variables
-
-#### MainWP Configuration
-
-| Variable | Description |
-|----------|-------------|
-| `MAINWP_APP_PASSWORD` | Application password for non-interactive login and commands when keychain is unavailable |
-| `MAINWPCONTROL_NO_KEYTAR` | Set to `1` to skip keytar (keychain) loading entirely |
-| `MAINWP_ALLOW_HTTP` | Set to `1` to allow insecure HTTP Dashboard URLs |
+## Configuration
-#### Chat/LLM Configuration
+Interactive use needs no configuration beyond `mainwpcontrol login`. For CI, Docker, and headless machines:
| Variable | Description |
|----------|-------------|
-| `ANTHROPIC_API_KEY` | Anthropic Claude |
-| `OPENAI_API_KEY` | OpenAI GPT |
-| `GOOGLE_API_KEY` | Google Gemini |
-| `OPENROUTER_API_KEY` | OpenRouter |
-| `LOCAL_LLM_API_KEY` | Local LLM provider (required to enable local provider) |
-| `LOCAL_LLM_URL` | Local endpoint URL (optional, defaults to localhost) |
-| `MAINWP_LLM_PROVIDER` | Override auto-detected provider |
-| `MAINWP_LLM_MODEL` | Specify model to use |
-
-### Configuration Settings
-
-All settings in `~/.config/mainwpcontrol/settings.json`:
-
-| Setting | Type | Description |
-|---------|------|-------------|
-| `defaultJsonOutput` | boolean | Default to JSON output |
-| `timeout` | number | HTTP request timeout in milliseconds |
-| `debug` | boolean | Enable debug output |
-| `llmProvider` | string | Default LLM provider for chat |
-| `chatContextMessages` | number | Max messages in chat context |
-| `skipSSLVerification` | boolean | Disable TLS verification (insecure, prefer per-profile setting via `login --skip-ssl-verify`) |
-| `allowInsecureHttp` | boolean | Allow `http://` Dashboard URLs without `MAINWP_ALLOW_HTTP=1` |
-
----
-
-## Troubleshooting
-
-
-"keytar failed to build" or native module errors during install
-
-Keytar requires native C++ compilation on some platforms. If it fails:
-
-1. **Use environment variable auth instead** (bypasses keytar entirely):
- ```bash
- export MAINWP_APP_PASSWORD='your-application-password'
- mainwpcontrol login --url https://dashboard.example.com --username admin
- ```
-2. **Or skip keytar explicitly** by setting `MAINWPCONTROL_NO_KEYTAR=1` before running commands.
-
-The pre-built binaries cover macOS, Windows, and Linux (x64/arm64). If you're on a different platform or architecture, you'll need C++ build tools (`gcc`, `g++`, `make`) or the env var approach.
+| `MAINWP_APP_PASSWORD` | Application Password for non-interactive login, and for commands when no OS keychain is available |
+| `MAINWPCONTROL_NO_KEYTAR` | Set to `1` to skip keychain loading entirely |
+| `MAINWP_ALLOW_HTTP` | Set to `1` to allow insecure `http://` Dashboard URLs |
-
-
-
-"command not found" after install
-
-This usually means your npm global bin directory isn't in your system PATH.
-
-1. **Find where npm installs global packages:**
- ```bash
- npm config get prefix
- ```
-2. **Add the `bin` subdirectory to your PATH.** For example, if the prefix is `/usr/local`:
- ```bash
- # Add to ~/.bashrc, ~/.zshrc, or your shell profile:
- export PATH="/usr/local/bin:$PATH"
- ```
-3. **Restart your terminal** (or run `source ~/.zshrc` / `source ~/.bashrc`) and try again.
+```bash
+export MAINWP_APP_PASSWORD='xxxx xxxx xxxx xxxx xxxx xxxx'
+mainwpcontrol login --url https://dashboard.example.com --username admin
+```
-On Windows, the npm global directory is usually already in PATH after installing Node.js.
+Optional defaults (JSON output, timeouts, chat provider) live in `~/.config/mainwpcontrol/settings.json`. The full list of settings, chat provider keys, and the credential storage model are in the [Configuration guide](docs/configuration.md).
-
+## Abilities
-
-"connection refused" or network errors
+Your Dashboard exposes its operations as versioned, self-describing "abilities" (the exact set varies by Dashboard version):
-If `mainwpcontrol login` or commands fail with connection errors:
+| Category | Abilities | Reference |
+| ---------------- | --------- | ----------------------------------------------------------------------------------------- |
+| Sites | 30 | [Sites Abilities](https://docs.mainwp.com/api-reference/abilities-api/sites) |
+| Updates | 13 | [Updates Abilities](https://docs.mainwp.com/api-reference/abilities-api/updates) |
+| Clients | 11 | [Clients Abilities](https://docs.mainwp.com/api-reference/abilities-api/clients) |
+| Tags | 7 | [Tags Abilities](https://docs.mainwp.com/api-reference/abilities-api/tags) |
+| Batch Operations | 1 | [Batch Operations](https://docs.mainwp.com/api-reference/abilities-api/batch-operations) |
-1. **Check the Dashboard URL.** Make sure it's the full URL with `https://` (e.g., `https://dashboard.example.com`). Don't include a trailing slash.
-2. **Verify HTTPS.** `mainwpcontrol` requires HTTPS by default. If your Dashboard uses HTTP (not recommended), set `MAINWP_ALLOW_HTTP=1`.
-3. **Check firewall/network.** Make sure your machine can reach the Dashboard:
- ```bash
- curl -I https://dashboard.example.com
- ```
-4. **SSL certificate issues.** If using a self-signed certificate, you can use `mainwpcontrol login --skip-ssl-verify` (not recommended for production).
+The table counts the Dashboard's built-in `mainwp/*` abilities. Extensions and other plugins can register their own, so your total may be higher (the 87 in the Quick Start sample comes from a Dashboard with extensions installed). `abilities list` shows what your Dashboard actually offers, `abilities info ` shows an ability's input schema and annotations, and `abilities run ` executes it. Because the CLI discovers abilities at runtime, new Dashboard capabilities appear without a CLI update.
-
+## Safety
----
+Every ability carries annotations that classify it as read-only, a write, or destructive, and the CLI enforces them. Destructive operations follow a two-step flow: a `--dry-run` preview shows what the Dashboard reports would be affected, and only an explicit `--confirm` executes it. The destructive class is deliberately wide: deletions, updates (`update-site-*`, `run-updates-*`, `update-all-*`), any ability that does not declare itself non-destructive, and a conservative name-based override that a mislabeling Dashboard cannot downgrade. The two flags are mutually exclusive, previews that fail block execution, and each confirmed run is written to a local audit log. Scripted pipelines that have already validated an operation can pass `--confirm --force` to skip the interactive prompt; that is a deliberate, per-command decision, never a default. Chat mode goes through the identical execution path, so an LLM can propose a destructive action but cannot confirm it. The full model is in [Safety & Destructive Operations](docs/safety.md).
## Contributing
```bash
-npm run build # Build the project
-npm test # Run tests (unit + e2e, no network needed)
-npm run lint # Check code style
+npm ci # install dependencies
+npm run build # build
+npm test # run tests (unit + e2e, no network needed)
+npm run lint # check code style
```
-### Live Integration Tests
+CI runs lint, type check, tests, and build on every pull request.
-`npm run test:live` runs tests against a real MainWP Dashboard, including workflow documentation validation. These require a running Dashboard and credentials:
+`npm run test:live` exercises a real Dashboard: API tests (login, discovery, read-only execution, safety model, exit codes) plus validation that every jq expression and field name in `docs/workflows/` works against the real API. It runs read-only operations and `--dry-run` previews only, never mutations, and needs credentials:
```bash
export MAINWP_API_URL=https://your-dashboard.example.com
@@ -587,23 +216,11 @@ export MAINWP_APP_PASSWORD='your-application-password'
npm run test:live
```
-The live suite includes API tests (login, abilities discovery, read-only execution, safety model, exit codes) and workflow doc tests (validates that every jq expression, field name, and data pipeline documented in `docs/workflows/` works against the real API).
-
-Live tests are safe: they only run read-only operations and `--dry-run` previews, never mutations.
-
----
+The full acceptance harness, which packs and installs the CLI as a consumer would, is described in [Acceptance Testing](docs/acceptance-testing.md).
## License
-GPL-3.0-or-later
-
----
-
-## Requirements
-
-- Node.js 20.18.1 or later
-- MainWP Dashboard 6+ with Abilities API
-- WordPress Application Password
+GPL-3.0-or-later. See [LICENSE](LICENSE).
---
diff --git a/docs/chat.md b/docs/chat.md
new file mode 100644
index 0000000..20ecfea
--- /dev/null
+++ b/docs/chat.md
@@ -0,0 +1,62 @@
+# Chat Mode
+
+`mainwpcontrol chat` lets you manage your Dashboard in plain English, using your own LLM API key:
+
+```bash
+export ANTHROPIC_API_KEY='sk-ant-...'
+
+mainwpcontrol chat
+mainwpcontrol chat "list all sites with pending updates"
+```
+
+Chat is optional. Every operation it can perform is also a plain `abilities run` command, and nothing else in the CLI depends on it. It earns its place for exploration: when you don't know which ability you need, or you want to chain a few lookups without writing the JSON yourself.
+
+## Providers
+
+Set one API key to enable chat. With multiple keys set, the provider is auto-detected; override with `MAINWP_LLM_PROVIDER` or `--provider`.
+
+| Variable | Provider |
+|----------|----------|
+| `ANTHROPIC_API_KEY` | Anthropic Claude |
+| `OPENAI_API_KEY` | OpenAI GPT |
+| `GOOGLE_API_KEY` | Google Gemini |
+| `OPENROUTER_API_KEY` | OpenRouter |
+| `LOCAL_LLM_API_KEY` | Local LLM (with optional `LOCAL_LLM_URL`, defaults to localhost) |
+
+`MAINWP_LLM_API_KEY` works as a generic key for whichever provider is selected. Pick a model with `MAINWP_LLM_MODEL` or `--model`. Persistent defaults (`llmProvider`, `chatContextMessages`) go in `~/.config/mainwpcontrol/settings.json`; see [Configuration](configuration.md#settings-file).
+
+## Flags
+
+| Flag | Description |
+|------|-------------|
+| `--provider` | Choose the LLM provider explicitly |
+| `--model` | Choose the model |
+| `--max-turns` | Limit conversation turns |
+| `--max-context-messages` | Limit messages kept in context |
+| `--no-stream` | Disable streamed responses |
+| `--api-key` | API key for the provider; prefer the environment variable, since flags are visible in the process list |
+| `--base-url` | Endpoint override for local or proxied providers |
+
+## Safety
+
+The model proposes; it never confirms or executes on its own. Chat runs abilities through the same execution path and policy as the CLI commands, so the classification and confirm rules in [Safety & Destructive Operations](safety.md) apply unchanged:
+
+- Read and write abilities the model calls are executed and the results returned to the conversation.
+- A destructive ability stops the conversation: you see a successful preview of what would be affected, and it runs only after your explicit approval at the terminal.
+- Approvals are single-use. Approving one deletion does not pre-approve the next one.
+- The model cannot set `confirm` or `dry_run` itself; those come from your terminal, never from the conversation.
+
+## What the provider sees
+
+Chat sends more to the LLM provider than the conversation text. Per request, the provider receives:
+
+- your messages and the model's own prior turns
+- every discovered ability's name, description, and input JSON schema, sent as tool definitions so the model knows what it can call
+- the inputs the model proposes for each call
+- ability results and error messages, with credential material redacted before serialization
+
+Your Application Password and your provider API key are never part of the payload. Ability results are your real site data, so treat chat the way you'd treat pasting that data into an AI tool, and pick your provider accordingly (including the local provider, which keeps everything on your machine).
+
+## Scripting note
+
+Chat is interactive by design. In non-TTY environments (pipes, CI), `mainwpcontrol chat` without a message argument exits with guidance instead of hanging. For automation, use `abilities run`; that's what it's for.
diff --git a/docs/cli-reference.md b/docs/cli-reference.md
new file mode 100644
index 0000000..e757436
--- /dev/null
+++ b/docs/cli-reference.md
@@ -0,0 +1,176 @@
+# CLI Reference
+
+The `mainwpcontrol` commands, their flags, and the exit-code contract. `mainwpcontrol --help` is always the authoritative listing for the version you have installed. For the credential and settings model behind these commands, see [Configuration](configuration.md).
+
+## Global flags
+
+These work on every command except the built-in `help` and `autocomplete` commands.
+
+| Flag | Description |
+|------|-------------|
+| `--json` | Structured JSON output: exactly one envelope on stdout |
+| `--quiet` / `-q` | Suppress output (exit code only) |
+| `--profile ` | Use a specific profile for this command |
+| `--debug` | Show redacted debug diagnostics on stderr |
+| `--help` | Show help |
+
+## `login`
+
+Connects to a Dashboard and creates a profile named after its hostname. Interactive by default; prompts for URL, username, and Application Password, then stores the credentials in your OS keychain.
+
+```bash
+# Interactive
+mainwpcontrol login
+
+# Non-interactive (CI, headless): password from the environment
+export MAINWP_APP_PASSWORD='xxxx xxxx xxxx xxxx xxxx xxxx'
+mainwpcontrol login --url https://dashboard.example.com --username admin
+```
+
+| Flag | Description |
+|------|-------------|
+| `--url ` | Dashboard URL (HTTPS required unless `MAINWP_ALLOW_HTTP=1` or the `allowInsecureHttp` setting is enabled) |
+| `--username ` | WordPress username |
+| `--name ` | Profile name (defaults to the Dashboard hostname) |
+| `--password ` | Application Password; prefer `MAINWP_APP_PASSWORD` or the prompt, since flags are visible in the process list |
+| `--skip-ssl-verify` | Accept a self-signed certificate for this profile (not for production) |
+
+When no OS keychain is available, credentials are not stored on disk; keep `MAINWP_APP_PASSWORD` set for each run.
+
+## `abilities list`
+
+Shows every operation your Dashboard exposes, grouped by category, with each one's type (read, write, destructive) and the exact `abilities run` command to execute it.
+
+```bash
+mainwpcontrol abilities list
+mainwpcontrol abilities list --category sites
+mainwpcontrol abilities list --json
+```
+
+## `abilities info`
+
+Shows one ability's description, input schema, and safety annotations.
+
+```bash
+mainwpcontrol abilities info list-sites-v1
+mainwpcontrol abilities info delete-site-v1 --json
+```
+
+## `abilities run`
+
+Executes an ability by its versioned name.
+
+```bash
+mainwpcontrol abilities run list-sites-v1 --json
+mainwpcontrol abilities run get-site-v1 --input '{"site_id_or_domain": 1}' --json
+mainwpcontrol abilities run get-site-v1 --input-file params.json --json
+echo '{"site_id_or_domain": 1}' | mainwpcontrol abilities run get-site-v1 --input - --json
+```
+
+| Flag | Description |
+|------|-------------|
+| `--input` / `-i` | Input parameters as JSON (use `-` for stdin) |
+| `--input-file` | Read input from a JSON file |
+| `--dry-run` | Preview a destructive ability without executing |
+| `--confirm` | Execute a destructive ability |
+| `--force` | Skip the interactive confirmation prompt (CI mode) |
+| `--wait` | Block until a batch job completes |
+| `--wait-timeout` | Max seconds to wait (default: 300) |
+
+`--dry-run` and `--confirm` are mutually exclusive. Destructive abilities refuse to run without one of them; the intended sequence is preview first, then confirm. The full flow, including what `--force` does and doesn't skip, is in [Safety & Destructive Operations](safety.md).
+
+## `jobs watch`
+
+Watches a batch job until it finishes. Operations that affect many items return a `job_id` immediately instead of blocking.
+
+```bash
+mainwpcontrol jobs watch
+mainwpcontrol jobs watch --timeout 120
+```
+
+| Flag | Description |
+|------|-------------|
+| `--timeout` | Max seconds to watch before giving up |
+| `--initial-delay` | First polling delay in milliseconds |
+| `--max-delay` | Polling backoff ceiling in milliseconds |
+| `--no-progress` | Suppress the progress display |
+
+A timeout or interruption leaves the job running on the Dashboard; re-run `jobs watch` with the same ID to pick it back up. Using `--wait` on the original `abilities run` command is equivalent to running the command and watching the job in one step.
+
+## `profile`
+
+Each `login` creates a profile, a named connection to one Dashboard, identified by hostname. Manage several Dashboards by logging in once per Dashboard.
+
+```bash
+mainwpcontrol profile list
+mainwpcontrol profile use production.example.com
+mainwpcontrol profile delete staging.example.com # also removes its keychain credentials
+```
+
+Any command accepts `--profile ` to target a profile without switching the default.
+
+`profile delete` always asks for confirmation and has no skip flag. In non-interactive contexts (pipes, CI) it cancels safely instead of deleting, so treat profile removal as a manual step.
+
+## `config show`
+
+Prints the active settings (from `~/.config/mainwpcontrol/settings.json` and defaults), with secrets redacted.
+
+```bash
+mainwpcontrol config show
+mainwpcontrol config show --verbose
+mainwpcontrol config show --json
+```
+
+## `doctor`
+
+Checks configuration, credentials, and Dashboard connectivity. Run it first when something isn't working.
+
+```bash
+mainwpcontrol doctor
+mainwpcontrol doctor -v # verbose
+mainwpcontrol doctor --json
+```
+
+## `chat`
+
+Talks to your Dashboard in plain English using your own LLM API key. Optional; nothing else depends on it.
+
+```bash
+export ANTHROPIC_API_KEY='sk-ant-...'
+mainwpcontrol chat
+mainwpcontrol chat "list all sites with pending updates"
+```
+
+Chat-specific flags: `--provider`, `--model`, `--max-turns`, `--max-context-messages`, `--no-stream`, `--api-key` (prefer the environment variable; flags are visible in the process list), and `--base-url` for local or proxy endpoints. Providers, models, and the chat safety model are in [Chat Mode](chat.md).
+
+## Shell completion
+
+```bash
+# Bash
+source "$(npm root -g)/@mainwp/control/scripts/completions/mainwpcontrol.bash"
+
+# Zsh
+source "$(npm root -g)/@mainwp/control/scripts/completions/mainwpcontrol.zsh"
+```
+
+## Exit codes
+
+| Code | Meaning | CI usage |
+|------|---------|----------|
+| 0 | Success | Continue pipeline |
+| 1 | User/input error | Fix command syntax |
+| 2 | Auth/config error | Check credentials |
+| 3 | Network error | Retry or check connectivity |
+| 4 | API error | Check ability parameters |
+| 5 | Internal error | Report bug |
+| 130 | Interrupted (SIGINT) | Ctrl-C during a prompt or `jobs watch`; standard Unix 128+signal convention, outside the 0-5 contract |
+| 143 | Terminated (SIGTERM) | `jobs watch` killed by a supervisor or timeout wrapper; same 128+signal convention |
+
+```bash
+# Pipeline branching on exit codes
+if mainwpcontrol abilities run check-sites-v1 --json --quiet; then
+ echo "All sites healthy"
+else
+ echo "Issues detected"
+fi
+```
diff --git a/docs/configuration.md b/docs/configuration.md
new file mode 100644
index 0000000..88d910c
--- /dev/null
+++ b/docs/configuration.md
@@ -0,0 +1,97 @@
+# Configuration
+
+How MainWP Control stores credentials, profiles, and settings, and every environment variable it reads.
+
+## Credential storage
+
+`mainwpcontrol login` stores your Application Password in the operating system keychain (macOS Keychain, Windows Credential Manager, libsecret on Linux). Pre-built keychain binaries are included for macOS, Windows, and Linux (x64 and arm64); on other platforms npm may need C++ build tools during installation.
+
+The profile file on disk records the Dashboard URL and username only. It never contains passwords.
+
+Stored credentials are bound to the Dashboard identity they were created for. If you upgraded from a 1.0.x beta and requests now fail with an authentication error, run `mainwpcontrol login` once per profile; see [Troubleshooting](troubleshooting.md#authentication-fails-after-upgrading).
+
+### Environment variable auth
+
+For CI, Docker, and machines without a keychain, put the password in the environment instead:
+
+```bash
+export MAINWP_APP_PASSWORD='xxxx xxxx xxxx xxxx xxxx xxxx'
+mainwpcontrol login --url https://dashboard.example.com --username admin
+```
+
+When no keychain is available, the password is never written to disk; keep `MAINWP_APP_PASSWORD` set for each run. The profile file is still written and records the Dashboard URL and username, as it does in every mode. If keytar is installed but broken, set `MAINWPCONTROL_NO_KEYTAR=1` to skip loading it.
+
+## Profiles
+
+A profile is a named connection to one Dashboard, created automatically by `login` and named after the hostname:
+
+```bash
+# Creates profile "staging.example.com"
+mainwpcontrol login --url https://staging.example.com --username admin
+
+# Creates profile "production.example.com"
+mainwpcontrol login --url https://production.example.com --username admin
+```
+
+Profiles live at `~/.config/mainwpcontrol/profiles.json`. Switch the default with `profile use`, target one per command with `--profile`, and remove one (including its keychain entry) with `profile delete`. Command details are in the [CLI Reference](cli-reference.md#profile).
+
+## Settings file
+
+Optional defaults live at `~/.config/mainwpcontrol/settings.json` (or `$XDG_CONFIG_HOME/mainwpcontrol/settings.json` if you set `XDG_CONFIG_HOME`):
+
+```json
+{
+ "defaultJsonOutput": true,
+ "timeout": 30000,
+ "debug": false,
+ "llmProvider": "openai",
+ "chatContextMessages": 20
+}
+```
+
+| Setting | Type | Description |
+|---------|------|-------------|
+| `defaultJsonOutput` | boolean | Default to JSON output |
+| `timeout` | number | HTTP request timeout in milliseconds |
+| `debug` | boolean | Enable debug output |
+| `llmProvider` | string | Default LLM provider for chat |
+| `chatContextMessages` | number | Max messages in chat context |
+| `skipSSLVerification` | boolean | Disable TLS verification (insecure; prefer the per-profile setting via `login --skip-ssl-verify`) |
+| `allowInsecureHttp` | boolean | Allow `http://` Dashboard URLs without `MAINWP_ALLOW_HTTP=1` |
+
+Inspect the active values with `mainwpcontrol config show`.
+
+## Environment variables
+
+### MainWP
+
+| Variable | Description |
+|----------|-------------|
+| `MAINWP_APP_PASSWORD` | Application Password for non-interactive login, and for commands when no keychain is available |
+| `MAINWPCONTROL_NO_KEYTAR` | Set to `1` to skip keytar (keychain) loading entirely |
+| `MAINWP_ALLOW_HTTP` | Set to `1` to allow insecure HTTP Dashboard URLs |
+
+### Chat / LLM
+
+| Variable | Description |
+|----------|-------------|
+| `ANTHROPIC_API_KEY` | Anthropic Claude |
+| `OPENAI_API_KEY` | OpenAI GPT |
+| `GOOGLE_API_KEY` | Google Gemini |
+| `OPENROUTER_API_KEY` | OpenRouter |
+| `LOCAL_LLM_API_KEY` | Local LLM provider (required to enable the local provider) |
+| `LOCAL_LLM_URL` | Local endpoint URL (optional, defaults to localhost) |
+| `MAINWP_LLM_API_KEY` | Generic API key for the selected provider (alternative to the provider-specific variables) |
+| `MAINWP_LLM_PROVIDER` | Override the auto-detected provider |
+| `MAINWP_LLM_MODEL` | Specify the model to use |
+
+Chat behavior and flags are covered in [Chat Mode](chat.md).
+
+## TLS and HTTP
+
+HTTPS is required by default. Two escape hatches exist for development environments:
+
+- `mainwpcontrol login --skip-ssl-verify` accepts a self-signed certificate for that profile.
+- `MAINWP_ALLOW_HTTP=1` (or `allowInsecureHttp` in settings) permits `http://` URLs.
+
+Both send credentials over connections an attacker on the network path can read or alter. Use them for local development against test Dashboards, never in production.
diff --git a/docs/getting-started.md b/docs/getting-started.md
new file mode 100644
index 0000000..5a836fb
--- /dev/null
+++ b/docs/getting-started.md
@@ -0,0 +1,75 @@
+# Getting Started
+
+This guide covers the background the rest of the documentation assumes: what a terminal is, what npm does, how Application Passwords work, and how to pass JSON on the command line. If you already live in a terminal, skip to the [README Quick Start](../README.md#quick-start).
+
+## What is a terminal?
+
+A terminal is where you type commands instead of clicking buttons. You'll see it called "command line" or "shell" in different places.
+
+**How to open it:**
+
+- **macOS**: Open **Terminal** (search in Spotlight, or look in Applications > Utilities)
+- **Windows**: Open **Git Bash** (installed with [Git for Windows](https://gitforwindows.org/)). If you don't have it, PowerShell works too; see the [quoting notes](#json-quoting-on-the-command-line) below.
+- **Linux**: Open your distribution's **Terminal** app (usually in the applications menu)
+
+## What does `npm install -g` do?
+
+`npm` is the Node.js package manager. It downloads and installs JavaScript packages. The `-g` flag installs globally, which makes `mainwpcontrol` available as a command anywhere on your system, not only in one project folder.
+
+```bash
+npm install -g @mainwp/control
+```
+
+If the command isn't found afterward, your npm global bin directory probably isn't on your PATH; see [Troubleshooting](troubleshooting.md#command-not-found-after-install).
+
+## What is an Application Password?
+
+WordPress Application Passwords let external tools like `mainwpcontrol` access your site without using your main login password. They look like groups of four characters separated by spaces (e.g., `abcd efgh ijkl mnop qrst uvwx`).
+
+**To create one:** Log into your MainWP Dashboard as an administrator > **Users > Profile** > scroll to **Application Passwords** > enter a name like "MainWP Control" > click **Add New Application Password** > copy the generated password. It is shown only once. You can revoke it from the same screen at any time, without touching your login password.
+
+Create a dedicated WordPress user for API access rather than using your main admin account. It keeps the audit trail clean and is easy to revoke later.
+
+## What is an environment variable?
+
+An environment variable is a named value that programs can read. They're commonly used for passwords and API keys.
+
+```bash
+# macOS / Linux / Git Bash (lasts until you close the terminal)
+export MAINWP_APP_PASSWORD='xxxx xxxx xxxx xxxx xxxx xxxx'
+
+# Windows PowerShell (lasts until you close the window)
+$env:MAINWP_APP_PASSWORD = 'xxxx xxxx xxxx xxxx xxxx xxxx'
+```
+
+For long-term storage, use the OS keychain (the default when you run `mainwpcontrol login`) or a restricted-permission `.env` file rather than pasting credentials into shell profile files. Note that `mainwpcontrol` does not read `.env` files itself: source the file (or export the variable another way) before running the CLI.
+
+## Reading command output
+
+- **`--json`** tells `mainwpcontrol` to print structured JSON on stdout, for scripting and piping to other tools.
+- **Exit codes** indicate success (`0`) or failure (`1` through `5`; interrupted commands exit `130` or `143`). You won't see them directly, but scripts and CI use them to decide what happens next. Run `echo $?` (macOS/Linux) or `echo $LASTEXITCODE` (PowerShell) after a command to check. The full table is in the [CLI Reference](cli-reference.md#exit-codes).
+
+## JSON quoting on the command line
+
+When you pass JSON with `--input`, quoting depends on your shell:
+
+```bash
+# macOS / Linux / Git Bash on Windows
+mainwpcontrol abilities run get-site-v1 --input '{"site_id_or_domain": 1}' --json
+```
+
+**Git Bash on Windows** handles quoting the same way macOS and Linux do. If you use Git Bash, all the examples in this documentation work without changes.
+
+**Windows PowerShell** quoting of inline JSON is unreliable: whether backslash-escaped quotes inside a single-quoted string reach the command intact depends on your PowerShell version. Don't fight it. Put your parameters in a file and use `--input-file`:
+
+```bash
+mainwpcontrol abilities run get-site-v1 --input-file params.json --json
+```
+
+This works the same on every platform. See [Input from File](workflows/input-from-file.md) for stdin and heredoc variants.
+
+## Next steps
+
+- Run through the [Quick Start](../README.md#quick-start) to connect your Dashboard
+- Browse the [CLI Reference](cli-reference.md) to see every command
+- Try a [workflow guide](workflows/) when you're ready to automate something real
diff --git a/docs/images/mainwp-control-fleet-from-the-terminal.png b/docs/images/mainwp-control-fleet-from-the-terminal.png
new file mode 100644
index 0000000..270f44e
Binary files /dev/null and b/docs/images/mainwp-control-fleet-from-the-terminal.png differ
diff --git a/docs/images/mainwp-control-guard-rails.png b/docs/images/mainwp-control-guard-rails.png
new file mode 100644
index 0000000..d8aa5d6
Binary files /dev/null and b/docs/images/mainwp-control-guard-rails.png differ
diff --git a/docs/images/mainwp-control-logo-2026.png b/docs/images/mainwp-control-logo-2026.png
new file mode 100644
index 0000000..a36619f
Binary files /dev/null and b/docs/images/mainwp-control-logo-2026.png differ
diff --git a/docs/safety.md b/docs/safety.md
new file mode 100644
index 0000000..bbc0a49
--- /dev/null
+++ b/docs/safety.md
@@ -0,0 +1,77 @@
+# Safety & Destructive Operations
+
+MainWP Control assumes that a command capable of deleting a client site deserves more friction than one that lists plugins. This page describes the model: how abilities are classified, what the preview-and-confirm flow requires, how batch jobs behave, and where the guard rails end.
+
+
+
+
+
+## Ability annotations
+
+Every ability your Dashboard exposes carries annotations, visible with `mainwpcontrol abilities info `:
+
+- **Readonly**: Safe to run anytime. Cannot modify data.
+- **Destructive**: Permanently changes or deletes data. Requires a `--dry-run` preview, then `--confirm` to execute.
+- **Idempotent**: Repeated calls have no additional effect under the same conditions. This describes the ability, and it is not permission to retry: a destructive confirm whose outcome is unknown is never re-run automatically, and you should verify Dashboard state before re-running it yourself (see below).
+
+Classification fails closed, twice over. An ability that does not declare itself non-destructive is treated as destructive. On top of that, a conservative name-based override forces the destructive class for known-dangerous name patterns (`delete-*`, `disconnect-*`, `suspend-*`, `update-site-*`, `run-updates-*`, `update-all-*`, `wipe-*`, and similar), whatever the Dashboard's annotations say, so a buggy or compromised server cannot downgrade a delete into a quiet write. In practice this means updates require the confirm flow; non-destructive writes like syncing sites do not.
+
+## The preview-and-confirm flow
+
+```bash
+# Step 1: Preview. Nothing changes.
+mainwpcontrol abilities run delete-site-v1 \
+ --input '{"site_id_or_domain": "mysite.com"}' \
+ --dry-run --json
+
+# Step 2: Execute, after reviewing the preview.
+mainwpcontrol abilities run delete-site-v1 \
+ --input '{"site_id_or_domain": "mysite.com"}' \
+ --confirm --json
+```
+
+The rules the CLI enforces:
+
+- `--dry-run` and `--confirm` are mutually exclusive. You cannot pass both.
+- A destructive ability without either flag does not run.
+- A preview shows what the Dashboard reports at preview time. Preview and execution are two independent calls with nothing binding them, so server state can change in between. What you approve is a preview taken immediately before execution; the execution itself runs against whatever the state is at confirm time.
+- A preview that fails blocks execution. The CLI never falls through to "run it anyway."
+- Safety flags come only from the command line. `dry_run` or `confirm` keys smuggled into `--input` JSON are stripped, so a parameter file (or an LLM composing one) cannot self-approve an operation.
+- Each confirmed execution is recorded in a local audit log before dispatch.
+
+`--force` skips the interactive "are you sure" prompt, nothing more. You still need `--confirm`, the ability still gets classified, and the audit entry is still written. Use it in CI where no terminal is attached:
+
+```bash
+mainwpcontrol abilities run delete-site-v1 \
+ --input '{"site_id_or_domain": "mysite.com"}' \
+ --confirm --force --json
+```
+
+## When the outcome is uncertain
+
+If the network fails after a confirm has been dispatched, the CLI does not retry. It reports the outcome as unknown, records that in the audit log, and exits with code 3. Retrying a delete that may have succeeded is worse than making you look: check the Dashboard state before running the command again.
+
+## Batch jobs
+
+When an operation affects enough items, the Dashboard queues it as a batch job instead of answering synchronously; the threshold is the Dashboard's decision, not the CLI's. The command returns a `job_id` immediately:
+
+```bash
+mainwpcontrol jobs watch
+mainwpcontrol jobs watch --timeout 120
+
+# Or block on the original command
+mainwpcontrol abilities run sync-sites-v1 --wait --wait-timeout 300 --json
+```
+
+A timeout reports the last known job status in its error details and leaves the job resumable by ID; a watch that gave up tells you what it knew, never "success." Interrupting with Ctrl-C (exit 130) or SIGTERM (exit 143) stops only the watch and reports the job ID; the Dashboard job keeps running, and `jobs watch ` picks it back up.
+
+## Chat mode
+
+Chat goes through the same execution path as the CLI commands. The LLM can propose a destructive action; executing one requires the same successful preview and your explicit, single-use approval at the terminal. A model cannot confirm on your behalf, and approval for one operation does not carry over to the next. Details in [Chat Mode](chat.md).
+
+## What the guard rails do not cover
+
+- **Non-destructive writes run without confirmation.** Syncing and reconnecting sites are writes that execute without the confirm flow. Updates are not in this group: the name-based override classifies `update-site-*`, `run-updates-*`, and `update-all-*` as destructive. The confirm flow is for the destructive class, not for every mutation.
+- **Annotations come from your Dashboard.** The CLI enforces them faithfully and treats undeclared abilities as destructive, but it cannot detect an ability that mislabels itself as read-only.
+- **Your Application Password is the real boundary.** The CLI adds friction and audit, but anyone holding the password can use the API directly. Use a dedicated WordPress user, and revoke its password if a machine is compromised.
+- **`--confirm --force` in a script is your judgment call.** The flags exist so validated pipelines can run unattended. Review what goes into those pipelines the way you'd review the command itself.
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
new file mode 100644
index 0000000..b922ace
--- /dev/null
+++ b/docs/troubleshooting.md
@@ -0,0 +1,74 @@
+# Troubleshooting
+
+Start with the built-in diagnostics before anything on this page:
+
+```bash
+mainwpcontrol doctor -v
+```
+
+`doctor` checks your configuration, credentials, and Dashboard connectivity, and names the failing layer.
+
+## "keytar failed to build" or native module errors during install
+
+Keytar (the keychain module) requires native C++ compilation on some platforms. Pre-built binaries cover macOS, Windows, and Linux (x64/arm64); elsewhere the build can fail. If it does:
+
+1. **Skip keytar and use environment variable auth.** `MAINWP_APP_PASSWORD` supplies the credential, but login still tries the keychain layer unless you disable it, so set both:
+ ```bash
+ export MAINWPCONTROL_NO_KEYTAR=1
+ export MAINWP_APP_PASSWORD='your-application-password'
+ mainwpcontrol login --url https://dashboard.example.com --username admin
+ ```
+2. **Or install C++ build tools** (`gcc`, `g++`, `make`) and reinstall.
+
+## "command not found" after install
+
+Your npm global bin directory isn't on your PATH.
+
+1. Find where npm installs global packages:
+ ```bash
+ npm config get prefix
+ ```
+2. Add the `bin` subdirectory to your PATH. For example, if the prefix is `/usr/local`:
+ ```bash
+ # Add to ~/.bashrc, ~/.zshrc, or your shell profile:
+ export PATH="/usr/local/bin:$PATH"
+ ```
+3. Restart your terminal (or run `source ~/.zshrc` / `source ~/.bashrc`) and try again.
+
+On Windows, the npm global directory is usually already in PATH after installing Node.js.
+
+## "connection refused" or network errors
+
+1. **Check the Dashboard URL.** Use the full URL with `https://` (e.g., `https://dashboard.example.com`), no trailing slash.
+2. **Verify HTTPS.** HTTPS is required by default. If your Dashboard uses HTTP (not recommended), set `MAINWP_ALLOW_HTTP=1`.
+3. **Check firewall/network.** Confirm your machine can reach the Dashboard:
+ ```bash
+ curl -I https://dashboard.example.com
+ ```
+4. **SSL certificate issues.** For a self-signed certificate, `mainwpcontrol login --skip-ssl-verify` accepts it for that profile (not for production).
+
+## Authentication fails after upgrading
+
+Version 1.1.0 binds stored credentials to the Dashboard identity they were created for, and refuses credentials saved by earlier beta versions. If commands that worked before the upgrade now fail with an authentication error, run:
+
+```bash
+mainwpcontrol login
+```
+
+once for each profile. This is a one-time migration; your Application Password itself is still valid.
+
+## Authentication fails on a fresh setup
+
+1. Confirm the Application Password was created on the **Dashboard** site, under the user you're logging in as, and copied exactly (spaces are fine either way).
+2. Confirm the user has administrator access to the MainWP Dashboard.
+3. Some security plugins and hosts disable Application Passwords; check for that if login rejects a freshly created password.
+
+## An ability fails with an API error (exit 4)
+
+1. Check the input schema: `mainwpcontrol abilities info ` shows required fields and types.
+2. Re-run with `--debug` to see redacted request diagnostics on stderr.
+3. Confirm the ability exists on your Dashboard version: `mainwpcontrol abilities list`. The set varies by Dashboard version, and the CLI only sees what the Dashboard exposes.
+
+## Still stuck?
+
+[Open an issue](https://github.com/mainwp/mainwp-control/issues) with the command you ran, the `--debug` stderr output, and your `doctor -v` output. Before posting, remove anything that identifies your setup: credentials and tokens, usernames, Dashboard URLs, and site names or IDs.