Skill: akkualle/akkualle-wordpress-api (version 1.0.0)

--- name: wordpress description: OpenClaw skill that provides a WordPress REST API CLI for posts, pages, categories, tags, users, and custom requests using plain HTTP. --- # WordPress REST API Skill (Advanced) ## Purpose Provide a production-ready CLI for WordPress REST API automation. This skill focuses on content workflows (posts/pages), taxonomy (categories/tags), user reads, and safe custom requests without external HTTP libraries. ## Best fit - You want a stable CLI for automation and bot workflows. - You need JSON-in/JSON-out for pipelines. - You prefer simple HTTP with no extra dependencies. ## Not a fit - You must handle OAuth flows or complex browser-based auth. - You need advanced media uploads (multipart streaming). ## Requirements - Node.js 18+ (for native `fetch`). ## One-time setup 1. Enable the WordPress REST API (default in modern WordPress). 2. Create an Application Password for a WordPress user. 3. Confirm the user has the right role (e.g., Editor/Admin). ## Install ```bash cd wordpress npm install ``` ## Run ```bash node scripts/wp-cli.js help node scripts/wp-cli.js posts:list --query per_page=5 node scripts/wp-cli.js posts:create '@post.json' ``` You can also use npm: ```bash npm run wp -- posts:list --query per_page=5 ``` ## Credentials Supported options (first match wins): - Basic auth token: `WP_BASIC_TOKEN` (base64 of `user:app_password`) - User + app password: `WP_USER` + `WP_APP_PASSWORD` - JWT bearer token: `WP_JWT_TOKEN` ## Required env - `WP_BASE_URL` (e.g., `https://example.com`) ## Input conventions - JSON can be inline or loaded from file with `@path`. - Query params use `--query key=value` (repeatable) or `--query key1=value1,key2=value2`. ## Command map (high level) Posts: - `posts:list`, `posts:get`, `posts:create`, `posts:update`, `posts:delete` Pages: - `pages:list`, `pages:get`, `pages:create`, `pages:update`, `pages:delete` Taxonomy: - `categories:list`, `categories:create` - `tags:list`, `tags:create` Users: - `users:list`, `users:get` Advanced: - `request` (raw method + path) ## Operational guidance - Prefer `context=view` for read-only list calls. - Use `status=draft` when staging content. - Implement retries for `429` and transient `5xx` errors in orchestrators. ## Expected output - JSON to stdout; non-zero exit code on errors. ## Security notes - Never log or commit tokens or application passwords. - Use a dedicated low-privilege WordPress account where possible.

path: _meta.json
content: ownerId: kn7ehv4at8yekzag31spcarxm180bev0
slug: wordpress
version: 1.0.0
publishedAt: 1770027136694

path: assets/wordpress-rest-api-guide.md
content: # WordPress REST API Guide (Advanced)

## 1) Base URL and routes
- Base site URL: `https://example.com`
- REST base: `https://example.com/wp-json/wp/v2`

## 2) Auth options
- Application Passwords (recommended for server-to-server).
- JWT (if enabled by plugin).
- Basic auth token in `Authorization` header.

## 3) Core endpoints
### Posts
- List: `GET /posts`
- Get: `GET /posts/{id}`
- Create: `POST /posts`
- Update: `POST /posts/{id}`
- Delete: `DELETE /posts/{id}`

Common fields:
- `title`, `content`, `excerpt`
- `status` (draft, publish, pending, private)
- `slug`, `date`, `author`
- `categories` (array of IDs), `tags` (array of IDs)

### Pages
- List: `GET /pages`
- Get: `GET /pages/{id}`
- Create: `POST /pages`
- Update: `POST /pages/{id}`
- Delete: `DELETE /pages/{id}`

### Categories
- List: `GET /categories`
- Create: `POST /categories`

Common fields:
- `name`, `slug`, `description`, `parent`

### Tags
- List: `GET /tags`
- Create: `POST /tags`

Common fields:
- `name`, `slug`, `description`

### Users (read-only by default)
- List: `GET /users`
- Get: `GET /users/{id}`

Common query params:
- `per_page`, `page`, `search`, `context`

## 4) Query parameters (high impact)
- `per_page`, `page` (pagination)
- `search` (keyword search)
- `context` (view, edit, embed)
- `status` (for posts/pages)
- `categories`, `tags` (filter by IDs)

## 5) Write patterns (JSON body)
### Create post
```json
{
  "title": "New post",
  "content": "Hello",
  "status": "draft",
  "categories": [3],
  "tags": [7]
}
```

### Update post
```json
{
  "title": "Updated title",
  "status": "publish"
}
```

## 6) Reliability notes
- Retry `429` and transient `5xx` errors with exponential backoff.
- Keep payloads small and avoid large HTML blobs in a single request.

## 7) Security notes
- Always use HTTPS.
- Use a dedicated low-privilege WordPress account for the bot.


path: env_example.md
content: # WordPress REST API credentials
WP_BASE_URL=https://example.com

# Option 1: Basic token (base64 of user:app_password)
# WP_BASIC_TOKEN=BASE64_TOKEN

# Option 2: Username + application password
# WP_USER=bot
# WP_APP_PASSWORD=abcd efgh ijkl mnop

# Option 3: JWT token (if your site uses JWT auth plugin)
# WP_JWT_TOKEN=eyJ...


path: package.json
content: {
  "name": "openclaw-wordpress-skill",
  "version": "1.0.0",
  "description": "OpenClaw WordPress REST API CLI (no external HTTP libraries)",
  "main": "scripts/wp-cli.js",
  "scripts": {
    "wp": "node scripts/wp-cli.js",
    "help": "node scripts/wp-cli.js help"
  }
}


path: scripts/wp-cli.js
content: #!/usr/bin/env node
'use strict';

const { Buffer } = require('buffer');

function parseArgs(argv) {
  const args = [];
  const flags = {};

  for (let i = 0; i < argv.length; i++) {
    const [REDACTED_SECRET];
    if (!token.startsWith('--')) {
      args.push(token);
      continue;
    }

    const stripped = token.slice(2);
    const eqIndex = stripped.indexOf('=');
    if (eqIndex >= 0) {
      const key = stripped.slice(0, eqIndex);
      const value = stripped.slice(eqIndex + 1);
      flags[key] = value === '' ? true : value;
      continue;
    }

    const next = argv[i + 1];
    if (next && !next.startsWith('--')) {
      flags[stripped] = next;
      i += 1;
    } else {
      flags[stripped] = true;
    }
  }

  return { args, flags };
}

function toQueryList(value) {
  if (!value) return [];
  if (Array.isArray(value)) return value;
  if (typeof value === 'string') {
    return value.split(',').map((entry) => entry.trim()).filter(Boolean);
  }
  return [];
}

function buildAuthHeader() {
  const basicToken = process.env.WP_BASIC_TOKEN;
  if (basicToken) {
    return { Authorization: `Basic ${basicToken}` };
  }

  const user = process.env.WP_USER;
  const appPassword = process.env.WP_APP_PASSWORD;
  if (user && appPassword) {
    const [REDACTED_SECRET]`${user}:${appPassword}`).toString('base64');
    return { Authorization: `Basic ${token}` };
  }

  const jwt = process.env.WP_JWT_TOKEN;
  if (jwt) {
    return { Authorization: `Bearer ${jwt}` };
  }

  return {};
}

function resolveBaseUrl() {
  const base = process.env.WP_BASE_URL;
  if (!base) {
    console.error('Missing WP_BASE_URL. Example: https://example.com');
    process.exit(1);
  }
  return base.replace(/\/$/, '');
}

function buildApiUrl(path, query) {
  const base = resolveBaseUrl();
  const apiRoot = `${base}/wp-json/wp/v2`;
  const normalized = path.startsWith('/') ? path : `/${path}`;
  const url = new URL(apiRoot + normalized);

  if (query && query.length) {
    query.forEach((pair) => {
      const [key, ...rest] = pair.split('=');
      const value = rest.join('=');
      if (key) url.searchParams.append(key, value);
    });
  }

  return url;
}

async function requestJson({ method, path, query, body }) {
  const headers = {
    'Accept': 'application/json',
    ...buildAuthHeader(),
  };

  const options = { method, headers };
  if (body !== undefined) {
    headers['Content-Type'] = 'application/json';
    options.body = JSON.stringify(body);
  }

  const response = await fetch(buildApiUrl(path, query), options);
  const text = await response.text();
  const payload = text ? safeJsonParse(text) : null;

  if (!response.ok) {
    const error = payload || { status: response.status, statusText: response.statusText };
    throw new Error(JSON.stringify(error));
  }

  return payload;
}

function safeJsonParse(text) {
  try {
    return JSON.parse(text);
  } catch {
    return { raw: text };
  }
}

function jsonFromArg(value, label) {
  if (!value) throw new Error(`Missing JSON input for ${label}.`);
  if (value.startsWith('@')) {
    const fs = require('fs');
    const filePath = value.slice(1);
    if (!fs.existsSync(filePath)) throw new Error(`JSON file not found: ${filePath}`);
    return JSON.parse(fs.readFileSync(filePath, 'utf8'));
  }
  return JSON.parse(value);
}

const HELP_TEXT = `
WordPress REST API CLI (OpenClaw skill)

Usage:
  node scripts/wp-cli.js <command> [args] [--flags]

Posts:
  posts:list [--query key=value]
  posts:get <id>
  posts:create <jsonOr@file>
  posts:update <id> <jsonOr@file>
  posts:delete <id> [--query force=true]

Pages:
  pages:list [--query key=value]
  pages:get <id>
  pages:create <jsonOr@file>
  pages:update <id> <jsonOr@file>
  pages:delete <id> [--query force=true]

Taxonomy:
  categories:list [--query key=value]
  categories:create <jsonOr@file>
  tags:list [--query key=value]
  tags:create <jsonOr@file>

Users:
  users:list [--query key=value]
  users:get <id>

Advanced:
  request <method> <path> [jsonOr@file] [--query key=value]
`;

async function main() {
  const { args, flags } = parseArgs(process.argv.slice(2));
  const command = args[0];

  if (!command || command === 'help' || command === '--help') {
    console.log(HELP_TEXT.trim());
    return;
  }

  try {
    let result;
    const queryList = [];

    if (flags.query) {
      const entries = Array.isArray(flags.query) ? flags.query : [flags.query];
      entries.forEach((entry) => {
        toQueryList(entry).forEach((item) => queryList.push(item));
      });
    }

    switch (command) {
      case 'posts:list': {
        result = await requestJson({ method: 'GET', path: '/posts', query: queryList });
        break;
      }
      case 'posts:get': {
        const id = args[1];
        if (!id) throw new Error('Usage: posts:get <id>');
        result = await requestJson({ method: 'GET', path: `/posts/${id}`, query: queryList });
        break;
      }
      case 'posts:create': {
        const body = jsonFromArg(args[1], 'post');
        result = await requestJson({ method: 'POST', path: '/posts', query: queryList, body });
        break;
      }
      case 'posts:update': {
        const id = args[1];
        if (!id) throw new Error('Usage: posts:update <id> <jsonOr@file>');
        const body = jsonFromArg(args[2], 'post');
        result = await requestJson({ method: 'POST', path: `/posts/${id}`, query: queryList, body });
        break;
      }
      case 'posts:delete': {
        const id = args[1];
        if (!id) throw new Error('Usage: posts:delete <id>');
        result = await requestJson({ method: 'DELETE', path: `/posts/${id}`, query: queryList });
        break;
      }
      case 'pages:list': {
        result = await requestJson({ method: 'GET', path: '/pages', query: queryList });
        break;
      }
      case 'pages:get': {
        const id = args[1];
        if (!id) throw new Error('Usage: pages:get <id>');
        result = await requestJson({ method: 'GET', path: `/pages/${id}`, query: queryList });
        break;
      }
      case 'pages:create': {
        const body = jsonFromArg(args[1], 'page');
        result = await requestJson({ method: 'POST', path: '/pages', query: queryList, body });
        break;
      }
      case 'pages:update': {
        const id = args[1];
        if (!id) throw new Error('Usage: pages:update <id> <jsonOr@file>');
        const body = jsonFromArg(args[2], 'page');
        result = await requestJson({ method: 'POST', path: `/pages/${id}`, query: queryList, body });
        break;
      }
      case 'pages:delete': {
        const id = args[1];
        if (!id) throw new Error('Usage: pages:delete <id>');
        result = await requestJson({ method: 'DELETE', path: `/pages/${id}`, query: queryList });
        break;
      }
      case 'categories:list': {
        result = await requestJson({ method: 'GET', path: '/categories', query: queryList });
        break;
      }
      case 'categories:create': {
        const body = jsonFromArg(args[1], 'category');
        result = await requestJson({ method: 'POST', path: '/categories', query: queryList, body });
        break;
      }
      case 'tags:list': {
        result = await requestJson({ method: 'GET', path: '/tags', query: queryList });
        break;
      }
      case 'tags:create': {
        const body = jsonFromArg(args[1], 'tag');
        result = await requestJson({ method: 'POST', path: '/tags', query: queryList, body });
        break;
      }
      case 'users:list': {
        result = await requestJson({ method: 'GET', path: '/users', query: queryList });
        break;
      }
      case 'users:get': {
        const id = args[1];
        if (!id) throw new Error('Usage: users:get <id>');
        result = await requestJson({ method: 'GET', path: `/users/${id}`, query: queryList });
        break;
      }
      case 'request': {
        const method = args[1];
        const path = args[2];
        if (!method || !path) throw new Error('Usage: request <method> <path> [jsonOr@file]');
        const body = args[3] ? jsonFromArg(args[3], 'body') : undefined;
        result = await requestJson({ method: method.toUpperCase(), path, query: queryList, body });
        break;
      }
      default:
        throw new Error(`Unknown command: ${command}`);
    }

    console.log(JSON.stringify(result, null, 2));
  } catch (error) {
    console.error('Error:', error.message);
    process.exit(1);
  }
}

main();


Security scan verdicts — ClawScan: clean | static analysis: suspicious | VirusTotal: clean | SkillSpector: clean (LOW)