Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:25:36 +08:00
commit cfadf66888
24 changed files with 4160 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
{
"name": "wordpress-ability-api",
"description": "Create, validate, and register WordPress Abilities (server-side PHP and client-side JavaScript) with comprehensive scaffolding and validation tools",
"version": "1.0.0",
"author": {
"name": "Em"
},
"skills": [
"./skills/wordpress-ability-api"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# wordpress-ability-api
Create, validate, and register WordPress Abilities (server-side PHP and client-side JavaScript) with comprehensive scaffolding and validation tools

125
plugin.lock.json Normal file
View File

@@ -0,0 +1,125 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:emdashcodes/wp-ability-toolkit:claude-code-plugins/wordpress-ability-api",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "b6a7bad8a96eeb26e08f742f47ee84eb151d01fc",
"treeHash": "c0405c887de8c97ee27d08cabba4e39e3679071c186716d49a1460d884166703",
"generatedAt": "2025-11-28T10:16:46.771418Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "wordpress-ability-api",
"description": "Create, validate, and register WordPress Abilities (server-side PHP and client-side JavaScript) with comprehensive scaffolding and validation tools",
"version": "1.0.0"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "a0d74add41d6ac9cd3a7c670bf8509ad0d6e74a5f9df6a3e084b8fee74b9e901"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "ba68aeb34960442e79b1a37da1cc5143f103f540d1b4708b57caaa3d40b13f90"
},
{
"path": "skills/wordpress-ability-api/SKILL.md",
"sha256": "89718dd4ed8ab3f5fcc4bb66afa81aaa5351ba0e833641845d66144de6c9597d"
},
{
"path": "skills/wordpress-ability-api/references/6.hooks.md",
"sha256": "fb730c49c7e926df9025f3d71b3eece5d30391078f1c56fcb29bb081097436f0"
},
{
"path": "skills/wordpress-ability-api/references/4.using-abilities.md",
"sha256": "1cab3c53221039caeb3861d6f4827ee55c0876023c1049c2b7ccd5d86a6835f0"
},
{
"path": "skills/wordpress-ability-api/references/2.getting-started.md",
"sha256": "23a78d5098968235ab4511a0e9ed5197a99165f36e7223bcf1aa3fa2c6176bc7"
},
{
"path": "skills/wordpress-ability-api/references/7.javascript-client.md",
"sha256": "291de8e0bc873872ba840d2c01c68825fd82added0f07868a79c954bed0aefb7"
},
{
"path": "skills/wordpress-ability-api/references/1.intro.md",
"sha256": "4da0f6d3b3d4c9367153483e6a7acdb12f34e61d4c24000c84ab5602c7e18b52"
},
{
"path": "skills/wordpress-ability-api/references/5.rest-api.md",
"sha256": "d4964942d7b3ff3a26df4cd616de6edbbf421a68ed46d9d3f79a21438428eb9e"
},
{
"path": "skills/wordpress-ability-api/references/7.registering-categories.md",
"sha256": "11e5c502402820c7d5a5aef50d06a1fc87e12ca39c253bf0fae3244a8c3a8b0f"
},
{
"path": "skills/wordpress-ability-api/references/3.registering-abilities.md",
"sha256": "2bcc92eb327295590f560ea7dbe9593f9e1cd77a8fd8510b816330a0a05b3462"
},
{
"path": "skills/wordpress-ability-api/scripts/scaffold-category.php",
"sha256": "153d07efe45458facf363a155d6e125cbccbec8c5868128d327037a528a3db4a"
},
{
"path": "skills/wordpress-ability-api/scripts/README.md",
"sha256": "05c0657af47d3f682c424f073eabc7f404284aa66f87dd527ab4df3c56313fc1"
},
{
"path": "skills/wordpress-ability-api/scripts/package.json",
"sha256": "7d9f14eef9866d834e9248865dab67f3fd4ebd3ac1b26e71a2da31d90e159244"
},
{
"path": "skills/wordpress-ability-api/scripts/validate-category.php",
"sha256": "a1a7a6e481bb9ba98578bd83b74e6d49badc7e795587fa6b114811450f250f17"
},
{
"path": "skills/wordpress-ability-api/scripts/validate-ability.php",
"sha256": "994c5621f20794c7fdff9c7b5a242769717e393a94e78ee82abb9ab8b368a279"
},
{
"path": "skills/wordpress-ability-api/scripts/scaffold-ability.php",
"sha256": "6ee7467f426b75b0112b32dc6897e1fd36d435a531d0c36dc0bdb66526d60a35"
},
{
"path": "skills/wordpress-ability-api/scripts/validate-ability.js",
"sha256": "d6c3d8253d9ea4737b1d68777de25de158acf6b65f82fc4897e25bc2e180ca80"
},
{
"path": "skills/wordpress-ability-api/scripts/validate-category.js",
"sha256": "5058d00da5e7a46092ba078f0a49da73054f00cfa93a5aaa9377159a5e377e9e"
},
{
"path": "skills/wordpress-ability-api/assets/client-category-template.js",
"sha256": "fc99e82c3da6bfbfd765818feb3c39f2596ab458e70b9e144994e5d9617d7d92"
},
{
"path": "skills/wordpress-ability-api/assets/server-ability-template.php",
"sha256": "941c375168336ba9d49fb8584adfa2935f4dea675354d623d3bb35c4b2b482f1"
},
{
"path": "skills/wordpress-ability-api/assets/category-template.php",
"sha256": "900791794e3237b0cba428da193e3164786a2a0504e094efb56e52f158c8024b"
},
{
"path": "skills/wordpress-ability-api/assets/client-ability-template.js",
"sha256": "100995b83daa5d723c483d63b8e4469b8a2c14bc6a12bbdf93ec0b41b63bb327"
}
],
"dirSha256": "c0405c887de8c97ee27d08cabba4e39e3679071c186716d49a1460d884166703"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

View File

@@ -0,0 +1,485 @@
---
name: wordpress-ability-api
description: This skill should be used when helping users create, edit, or register WordPress Abilities (both server-side PHP and client-side JavaScript), register ability categories, or set up the WordPress Abilities API as a dependency. Use when users say things like "create an ability", "help me build an ability", "set up the Ability API", or "register a category".
---
# WordPress Ability API
## Overview
This skill helps users work with the WordPress Abilities API - a standardized system for registering and discovering distinct units of functionality within WordPress. Use this skill to help users create server-side PHP abilities, client-side JavaScript abilities, register categories, and set up the API as a dependency.
The Abilities API makes WordPress functionality discoverable by AI agents and automation tools through machine-readable schemas, permissions, and validation.
## When to Use This Skill
Activate this skill when the user requests:
- **Creating abilities**: "Help me create an ability to...", "I want to make an ability that...", "Build me an ability for..."
- **Editing abilities**: "Update my ability", "Fix this ability code", "Modify the ability to..."
- **Setting up the API**: "Set up the Ability API", "Add the Ability API as a dependency", "Install the Abilities API"
- **Registering categories**: "Create a category for my abilities", "Register an ability category"
## Workflow Decision Tree
When this skill activates, determine what the user needs:
1. **Are they creating/editing an ability?**
→ Follow the "Creating Abilities" workflow below
2. **Do they need to set up the API first?**
→ Follow the "Setting Up the API" workflow below
3. **Are they just learning about the API?**
→ Reference the documentation in `references/` and explain concepts
## Using the Scaffold Script
The skill includes a `scripts/scaffold-ability.php` script designed for programmatic ability generation. Use this when creating abilities to quickly generate validated boilerplate code.
### Script Usage
```bash
php scripts/scaffold-ability.php \
--name="plugin/ability-name" \
--type="server" \
--category="data-retrieval" \
--label="Optional Label" \
--description="Optional description"
```
**Required Arguments:**
- `--name` - Ability name in format "namespace/ability-name"
- `--type` - Either "server" (PHP) or "client" (JavaScript)
- `--category` - Category slug (e.g., "data-retrieval", "data-modification")
**Optional Arguments:**
- `--label` - Human-readable label (auto-generated from name if omitted)
- `--description` - Detailed description (placeholder if omitted)
- `--readonly` - "true" or "false" (default: false)
- `--destructive` - "true" or "false" (default: true)
- `--idempotent` - "true" or "false" (default: false)
**Output:** Complete ability code printed to stdout, ready to be written to plugin files.
### When to Use the Scaffold Script
Use the scaffold script after gathering requirements from the user (Steps 1-3 of the workflow below). The script generates clean, validated boilerplate that follows best practices.
**Important:** Always gather required information from the user BEFORE calling the script, or generate sensible defaults based on context.
## Creating Abilities
When helping users create or edit abilities, follow this conversational workflow:
### Step 1: Understand the Functionality
Ask clarifying questions to understand what the ability should do:
- **What should this ability do?** (e.g., "Get site analytics", "Send notifications", "Update settings")
- **Is this a server-side or client-side ability?**
- **Server-side (PHP)**: Runs on the WordPress backend, accesses database, uses WordPress functions
- **Client-side (JavaScript)**: Runs in the browser, manipulates DOM, handles UI interactions
- **What input parameters does it need?** (e.g., user ID, date range, options)
- **What should it return?** (e.g., array of data, success boolean, error messages)
- **Who should be able to use it?** (permissions: any user, logged-in users, administrators, custom capability)
### Step 2: Determine Plugin Location
Ask where the ability code should live:
- **"Which plugin should this ability be registered in?"**
- If they have an existing plugin, use that
- If they need a new plugin, offer to create one using the `wordpress-plugin-scaffold` skill
### Step 3: Check for Category
Abilities must belong to a category. Ask:
- **"Which category should this ability belong to?"**
- Example categories: `data-retrieval`, `data-modification`, `communication`, `ecommerce`, `user-management`
- If they need a custom category, use `scripts/scaffold-category.php` to generate it:
```bash
php scripts/scaffold-category.php \
--name="custom-category" \
--label="Custom Category" \
--description="Description of what abilities belong here" \
--type="server|client"
```
- Alternatively, manually customize `assets/category-template.php`
- See `references/7.registering-categories.md` for detailed category registration information
### Step 4: Generate the Ability Code
Use the scaffold script to generate complete, validated code:
```bash
php scripts/scaffold-ability.php \
--name="namespace/ability-name" \
--type="server|client" \
--category="category-slug" \
--label="Human Label" \
--description="Detailed description" \
--readonly="true|false" \
--destructive="true|false" \
--idempotent="true|false"
```
**For server-side (PHP):** Outputs complete PHP code with callback and registration functions
**For client-side (JavaScript):** Outputs complete JavaScript code with async callback
Alternatively, manually copy and customize templates from `assets/` directory:
- `server-ability-template.php` - PHP abilities
- `client-ability-template.js` - JavaScript abilities
Replace all `{{PLACEHOLDERS}}` with actual values. See templates for full list of placeholders.
### Step 5: Validate the Code
Before adding the code to plugin files, validate it using the appropriate validation script:
#### Validate Abilities
**For PHP abilities:**
```bash
php scripts/validate-ability.php path/to/ability-file.php
```
**For JavaScript abilities:**
```bash
node scripts/validate-ability.js path/to/ability-file.js
```
#### Validate Categories
**For PHP categories:**
```bash
php scripts/validate-category.php path/to/category-file.php
```
**For JavaScript categories:**
```bash
node scripts/validate-category.js path/to/category-file.js
```
All validators check:
- **Structure**: Required fields (name, label, description, schemas/callbacks) are present
- **JSON Schema validity**: Schemas follow JSON Schema specification (abilities only)
- **Best practices**: Proper naming (kebab-case), annotations, permission callbacks
- **Common mistakes**: TODO placeholders, empty descriptions, security concerns
**Exit codes:**
- `0` - Validation passed
- `1` - Validation failed (errors found)
- `2` - File not found or invalid usage
Fix any errors reported by the validator before proceeding.
### Step 6: Add to Plugin Files
Write the validated code to the appropriate plugin files:
- **PHP abilities**: Add to plugin's main PHP file or create a dedicated `includes/abilities.php` file and hook into `abilities_api_init`
- **JavaScript abilities**: Add to enqueued JavaScript file with `@wordpress/abilities` dependency
- **Categories**: Register on `abilities_api_categories_init` hook before abilities
Use the Write tool to add the code to the correct location in the plugin.
### Step 7: Test the Ability
After code is added to plugin files, suggest testing approaches:
**Server-side (PHP):**
- Use `wp_get_ability('namespace/name')` to verify registration
- Test execution via REST API: `POST /wp-json/abilities/v1/abilities/{namespace}/{ability-name}/execute`
- Or test directly: `$ability->execute( $input_data )`
- Verify permissions work as expected
- Check input validation catches invalid data
**Client-side (JavaScript):**
- Open browser DevTools console
- Check registration: `wp.data.select('core/abilities').getAbility('namespace/ability-name')`
- Execute ability: `wp.data.dispatch('core/abilities').executeAbility('namespace/ability-name', inputData)`
- Check available abilities: `wp.data.select('core/abilities').getAbilities()`
- Verify permissions and error handling work correctly
## Setting Up the API
The WordPress Abilities API must be available before abilities can be registered. Help users set up the API based on their WordPress version:
### Check WordPress Version
**WordPress 6.9+**: The Abilities API is included in core - no setup needed!
**WordPress < 6.9**: The API must be installed as a plugin or dependency.
### Installation Options
Reference `references/2.getting-started.md` for detailed setup instructions. Guide users through the most appropriate method:
#### Option 1: As a Plugin (Easiest for Testing)
If they're using **wp-env** (check for `.wp-env.json`), help them add it via the `wp-env` skill:
```json
{
"plugins": ["WordPress/abilities-api"]
}
```
If they're using **WP-CLI**:
```bash
wp plugin install https://github.com/WordPress/abilities-api/releases/latest/download/abilities-api.zip
wp plugin activate abilities-api
```
#### Option 2: As a Plugin Dependency (Recommended for Plugins)
Help them add to their plugin header:
```php
/**
* Plugin Name: My Plugin
* Requires Plugins: abilities-api
*/
```
Then add availability check:
```php
if ( ! class_exists( 'WP_Ability' ) ) {
add_action( 'admin_notices', function() {
wp_admin_notice(
__( 'This plugin requires the Abilities API. Please install and activate it.', 'my-plugin' ),
'error'
);
} );
return;
}
```
#### Option 3: As a Composer Dependency
```bash
composer require wordpress/abilities-api
```
### Verify Installation
Help users verify the API is loaded:
```php
if ( class_exists( 'WP_Ability' ) ) {
// API is available
echo 'Abilities API version: ' . WP_ABILITIES_API_VERSION;
}
```
## Integration with Other Skills
This skill works well with:
- **wordpress-plugin-scaffold**: Use when users need a new plugin to register abilities in
- **wp-env**: Use for local development environment setup when installing the Abilities API
## Reference Documentation
The `references/` directory contains complete API documentation:
- **1.intro.md**: Core concepts, goals, and benefits of the Abilities API
- **2.getting-started.md**: Installation methods and basic usage examples
- **3.registering-abilities.md**: Comprehensive guide to `wp_register_ability()` with all parameters and examples
- **4.using-abilities.md**: How to retrieve and execute abilities in PHP
- **5.rest-api.md**: REST API endpoints for abilities
- **6.hooks.md**: Available WordPress hooks in the Abilities API
- **7.javascript-client.md**: Complete client-side JavaScript API reference
- **7.registering-categories.md**: How to register ability categories
**When to read references:**
- For specific parameter details → `3.registering-abilities.md`
- For setup instructions → `2.getting-started.md`
- For client-side abilities → `7.javascript-client.md`
- For category registration → `7.registering-categories.md`
- For conceptual understanding → `1.intro.md`
## Scripts and Templates
### Scripts (`scripts/`)
The skill includes automation scripts for generating and validating both abilities and categories:
#### Ability Scripts
- **scaffold-ability.php**: Programmatically generate ability code from CLI arguments
- Accepts: name, type, category, label, description, and annotation flags
- Outputs: Complete, validated ability code to stdout
- Usage: `php scripts/scaffold-ability.php --name="plugin/ability" --type="server" --category="data-retrieval"`
- Use when: Creating new abilities after gathering requirements
- **validate-ability.php**: Validate PHP ability code independently of WordPress
- Checks: Required fields, JSON Schema validity, best practices
- Outputs: Detailed validation results with errors and warnings
- Usage: `php scripts/validate-ability.php path/to/ability-file.php`
- Use when: Validating server-side PHP ability code before registration
- **validate-ability.js**: Validate JavaScript ability registration code
- Requirements: Node.js and acorn parser (`npm install acorn`)
- Parser: Uses acorn AST parser for accurate JavaScript parsing
- Checks: Required fields (name, category, callback), name format, schemas, permissions, annotations
- Outputs: Detailed validation results with errors and warnings
- Usage: `node scripts/validate-ability.js path/to/ability-file.js`
- Use when: Validating client-side JavaScript ability code before registration
#### Category Scripts
- **scaffold-category.php**: Programmatically generate category registration code
- Accepts: name, label, description, type (server/client)
- Outputs: Complete category registration code to stdout
- Usage: `php scripts/scaffold-category.php --name="category-slug" --label="Label" --description="Description" --type="server"`
- Use when: Creating custom categories for abilities
- Includes helpful comments about registration hooks and timing
- **validate-category.php**: Validate PHP category registration code
- Parser: Uses PHP's `token_get_all()` for accurate parsing
- Checks: Name format (kebab-case), required fields, description quality
- Outputs: Detailed validation results with errors and warnings
- Usage: `php scripts/validate-category.php path/to/category-file.php`
- Use when: Validating server-side PHP category code
- **validate-category.js**: Validate JavaScript category registration code
- Requirements: Node.js and acorn parser (`npm install acorn`)
- Parser: Uses acorn AST parser for accurate JavaScript parsing
- Checks: Same validations as PHP validator
- Outputs: Detailed validation results with errors and warnings
- Usage: `node scripts/validate-category.js path/to/category-file.js`
- Use when: Validating client-side JavaScript category code
### Template Assets (`assets/`)
The skill also includes manual starter templates:
- **server-ability-template.php**: Complete boilerplate for server-side PHP abilities
- **client-ability-template.js**: Complete boilerplate for client-side JavaScript abilities
- **category-template.php**: Template for server-side PHP category registration
- **client-category-template.js**: Template for client-side JavaScript category registration
**Note:** Templates use `{{PLACEHOLDER}}` syntax and are read by scaffold scripts. You can also manually copy and replace placeholders for custom needs. Scaffold scripts generate code by loading these templates and replacing placeholders with actual values.
## Best Practices
When helping users create abilities:
1. **Use descriptive names**: `my-plugin/get-user-analytics` not `my-plugin/analytics`
2. **Write detailed descriptions**: Help AI agents understand WHEN and HOW to use the ability
3. **Define complete schemas**: Use JSON Schema to validate inputs and document outputs
4. **Implement proper permissions**: Never use `__return_true` for sensitive operations
5. **Use appropriate annotations**:
- `readonly: true` for data retrieval abilities
- `destructive: false` for abilities that only add/update (never delete)
- `idempotent: true` for abilities that can be called repeatedly safely
6. **Handle errors gracefully**: Return `WP_Error` objects with clear error messages
7. **Test thoroughly**: Verify schema validation, permissions, and error cases
## Common Patterns
### Read-Only Data Retrieval
```php
'meta' => array(
'annotations' => array(
'readonly' => true,
'destructive' => false,
'idempotent' => true,
),
)
```
### Data Modification (Non-Destructive)
```php
'meta' => array(
'annotations' => array(
'readonly' => false,
'destructive' => false,
'idempotent' => false,
),
)
```
### Potentially Destructive Operations
```php
'meta' => array(
'annotations' => array(
'readonly' => false,
'destructive' => true,
'idempotent' => false,
),
),
'permission_callback' => function() {
return current_user_can( 'manage_options' );
}
```
## Advanced Features
### Client-Side Category Registration
When registering client-side abilities with custom categories, use `registerAbilityCategory()` before registering the ability:
```javascript
import { registerAbilityCategory, registerAbility } from '@wordpress/abilities';
// Register the category first
await registerAbilityCategory('my-custom-category', {
label: 'My Custom Category',
description: 'Description of what abilities belong in this category',
meta: {
icon: 'dashicons-admin-customizer',
priority: 10,
},
});
// Then register abilities using that category
await registerAbility({
name: 'my-plugin/my-ability',
category: 'my-custom-category', // Uses the client-registered category
// ... other properties
});
```
### Custom Ability Classes (Advanced)
For advanced use cases requiring custom behavior, specify a custom class that extends `WP_Ability` using the `ability_class` parameter:
```php
class Custom_Ability extends WP_Ability {
// Override methods for custom behavior
public function execute( $input = array() ) {
// Custom execution logic
return parent::execute( $input );
}
}
wp_register_ability( 'my-plugin/custom-ability', array(
// ... standard parameters
'ability_class' => Custom_Ability::class, // Use custom class instead of WP_Ability
) );
```
**Note:** This is an advanced feature. Most abilities should use the default `WP_Ability` class.

View File

@@ -0,0 +1,20 @@
<?php
/**
* Register ability category: {{LABEL}}
*
* {{DESCRIPTION}}
*/
/**
* Hook: abilities_api_categories_init
* Categories must be registered BEFORE abilities that use them.
* This hook fires before 'abilities_api_init'.
*/
add_action( 'abilities_api_categories_init', '{{REGISTER_FUNCTION}}' );
function {{REGISTER_FUNCTION}}() {
wp_register_ability_category( '{{CATEGORY_NAME}}', array(
'label' => __( '{{LABEL}}', '{{NAMESPACE}}' ),
'description' => __( '{{DESCRIPTION}}', '{{NAMESPACE}}' ),
) );
}

View File

@@ -0,0 +1,73 @@
/**
* Client-Side Ability: {{LABEL}}
*
* {{DESCRIPTION}}
*/
import { registerAbility } from '@wordpress/abilities';
/**
* Register the {{ABILITY_NAME}} ability.
*/
registerAbility( {
name: '{{ABILITY_NAME}}',
label: '{{LABEL}}',
description: '{{DESCRIPTION}}',
category: '{{CATEGORY}}',
// Define expected input structure using JSON Schema
inputSchema: {
type: 'object',
properties: {
// TODO: Define input parameters
// param1: {
// type: 'string',
// description: 'Description of parameter',
// },
},
required: [], // TODO: List required parameters
additionalProperties: false,
},
// Define output structure using JSON Schema
outputSchema: {
type: 'object',
properties: {
result: {
type: 'string',
description: 'The result of the operation',
},
},
},
// The async callback function to execute
callback: async ( input ) => {
// TODO: Implement your ability logic here
// Example error handling:
// if ( ! someValidation( input ) ) {
// throw new Error( 'The provided input is invalid.' );
// }
return {
result: 'TODO: Implement logic',
};
},
// Permission check callback
permissionCallback: ( input ) => {
// TODO: Implement appropriate permission checks
// Examples:
// return window.wp?.data?.select('core')?.getCurrentUser() !== null;
return true; // Everyone can access (use with caution!)
},
// Metadata
meta: {
annotations: {
readonly: {{READONLY}},
destructive: {{DESTRUCTIVE}},
idempotent: {{IDEMPOTENT}},
},
},
} );

View File

@@ -0,0 +1,15 @@
/**
* Register ability category: {{LABEL}}
*
* {{DESCRIPTION}}
*
* Note: This must be called BEFORE registering abilities that use this category.
* Typically placed at the top of your abilities registration file.
*/
import { registerAbilityCategory } from '@wordpress/abilities';
await registerAbilityCategory('{{CATEGORY_NAME}}', {
label: '{{LABEL}}',
description: '{{DESCRIPTION}}',
});

View File

@@ -0,0 +1,88 @@
<?php
/**
* Ability: {{LABEL}}
*
* {{DESCRIPTION}}
*/
/**
* Execute callback for {{ABILITY_NAME}} ability.
*
* @param array|mixed $input Input data matching the input_schema.
* @return mixed Output data matching the output_schema, or WP_Error on failure.
*/
function {{CALLBACK_FUNCTION}}( $input ) {
// TODO: Implement your ability logic here
// Example error handling:
// if ( ! some_validation( $input ) ) {
// return new WP_Error(
// 'invalid_input',
// __( 'The provided input is invalid.', '{{NAMESPACE}}' )
// );
// }
return array(
'result' => 'TODO: Implement logic',
);
}
/**
* Register the {{ABILITY_NAME}} ability.
*/
add_action( 'abilities_api_init', '{{REGISTER_FUNCTION}}' );
function {{REGISTER_FUNCTION}}() {
wp_register_ability( '{{ABILITY_NAME}}', array(
'label' => __( '{{LABEL}}', '{{NAMESPACE}}' ),
'description' => __( '{{DESCRIPTION}}', '{{NAMESPACE}}' ),
'category' => '{{CATEGORY}}',
// Define expected input structure using JSON Schema
'input_schema' => array(
'type' => 'object',
'properties' => array(
// TODO: Define input parameters
// 'param1' => array(
// 'type' => 'string',
// 'description' => __( 'Description of parameter', '{{NAMESPACE}}' ),
// ),
),
'required' => array(), // TODO: List required parameters
'additionalProperties' => false,
),
// Define output structure using JSON Schema
'output_schema' => array(
'type' => 'object',
'properties' => array(
'result' => array(
'type' => 'string',
'description' => __( 'The result of the operation', '{{NAMESPACE}}' ),
),
),
),
// The callback function to execute
'execute_callback' => '{{CALLBACK_FUNCTION}}',
// Permission check callback
'permission_callback' => function( $input ) {
// TODO: Implement appropriate permission checks
// Examples:
// return current_user_can( 'manage_options' );
// return is_user_logged_in();
return __return_true(); // Everyone can access (use with caution!)
},
// Metadata
'meta' => array(
'show_in_rest' => true,
'annotations' => array(
'readonly' => {{READONLY}},
'destructive' => {{DESTRUCTIVE}},
'idempotent' => {{IDEMPOTENT}},
),
),
) );
}

View File

@@ -0,0 +1,99 @@
# 1. Introduction & Overview
## What is the Abilities API?
The WordPress Abilities API provides a standardized way to register and discover distinct units of functionality within a WordPress site. These units, called "Abilities", represent specific actions or capabilities that components can perform, with clearly defined inputs, outputs, and permissions.
It acts as a central registry, making it easier for different parts of WordPress, third-party plugins, themes, and external systems (like AI agents) to understand and interact with the capabilities available on a specific site.
## Core Concepts
- **Ability:** A distinct piece of functionality with a unique name following the `namespace/ability-name` pattern. Each ability has a human-readable name and description, input/output definitions (using JSON Schema), a category assignment, optional permissions, and an associated callback function for execution. Each registered Ability is an instance of the `WP_Ability` class.
- **Category:** A way to organize related abilities. Each ability must belong to exactly one category. Categories have a slug, label, and description. Each registered category is an instance of the `WP_Ability_Category` class.
- **Registry:** A central, singleton object (`WP_Abilities_Registry`) that holds all registered abilities. It provides methods for registering, unregistering, finding, and querying abilities. Similarly, `WP_Abilities_Category_Registry` manages all registered categories.
- **Callback:** The PHP function or method executed when an ability is called via `WP_Ability::execute()`.
- **Schema:** JSON Schema definitions for an ability's expected input (`input_schema`) and its returned output (`output_schema`). This allows for validation and helps agents understand how to use the ability.
- **Permission Callback:** An optional function that determines if the current user can execute a specific ability.
- **Namespace:** The first part of an ability name (before the slash), typically matching the plugin or component name that registers the ability.
## Goals and Benefits
- **Standardization:** Provides a single, consistent way to expose site capabilities.
- **Discoverability:** Makes functionality easily discoverable by AI systems and automation tools.
- **Validation:** Built-in input/output validation using JSON Schema ensures data integrity.
- **Security:** Permission callbacks provide fine-grained access control.
- **Extensibility:** Simple registration pattern allows any plugin or theme to expose their capabilities.
- **AI-Friendly:** Machine-readable format enables intelligent automation and AI agent interactions.
## Use Cases
- **AI Integration:** Allow AI agents to discover and interact with site capabilities.
- **Plugin Interoperability:** Enable plugins to discover and use each other's functionality.
- **Automation Tools:** Provide programmatic access to site features.
- **API Documentation:** Self-documenting capabilities with schema validation.
- **Developer Tools:** Standardized way to expose plugin functionality.
## Registration Example
```php
// First, register a category, or use one of the existing categories.
add_action( 'abilities_api_categories_init', 'my_plugin_register_category');
function my_plugin_register_category(){
wp_register_ability_category( 'site-information', array(
'label' => __( 'Site Information', 'my-plugin' ),
'description' => __( 'Abilities that provide information about the WordPress site.', 'my-plugin' ),
));
}
// Then, register an ability in that category
add_action( 'abilities_api_init', 'my_plugin_register_ability');
function my_plugin_register_ability(){
wp_register_ability( 'my-plugin/site-info', array(
'label' => __( 'Site Info', 'my-plugin' ),
'description' => __( 'Returns information about this WordPress site', 'my-plugin' ),
'category' => 'site-information',
'input_schema' => array(),
'output_schema' => array(
'type' => 'object',
'properties' => array(
'site_name' => array(
'type' => 'string',
'description' => __( 'The name of the WordPress site', 'my-plugin' )
),
'site_url' => array(
'type' => 'string',
'description' => __( 'The URL of the WordPress site', 'my-plugin' )
),
'active_theme' => array(
'type' => 'string',
'description' => __( 'The active theme of the WordPress site', 'my-plugin' )
),
'active_plugins' => array(
'type' => 'array',
'items' => array(
'type' => 'string'
),
'description' => __( 'List of active plugins on the WordPress site', 'my-plugin' )
),
'php_version' => array(
'type' => 'string',
'description' => __( 'The PHP version of the WordPress site', 'my-plugin' )
),
'wordpress_version' => array(
'type' => 'string',
'description' => __( 'The WordPress version of the site', 'my-plugin' )
)
),
),
'execute_callback' => 'my_plugin_get_siteinfo',
'permission_callback' => function( $input ) {
return current_user_can( 'manage_options' );
},
'meta' => array(
'show_in_rest' => true,
),
) );
}
```
This creates a machine-readable capability that AI systems and automation tools can discover, understand, and execute safely within the bounds of WordPress permissions and validation rules.

View File

@@ -0,0 +1,150 @@
# 2. Getting Started
## Installation
Until the Abilities API is merged into WordPress core, it must be installed before it can be used.
### As a plugin
The easiest way to try and use the Abilities API is to install it as a plugin by downloading the latest release from the [GitHub Releases page](https://github.com/WordPress/abilities-api/releases/latest).
#### With WP-CLI
```bash
wp plugin install https://github.com/WordPress/abilities-api/releases/latest/download/abilities-api.zip
```
#### With WP-Env
```jsonc
// .wp-env.json
{
"$schema": "https://schemas.wp.org/trunk/wp-env.json",
// ... other config ...
"plugins": [
"WordPress/abilities-api"
// ... other plugins ...
]
// ... more config ...
}
```
### As a dependency
Plugin authors and developers may wish to rely on the Abilities API as a dependency in their own projects, before it is merged into core. You can do that in one of the following ways.
#### As a Plugin Dependency (Recommended)
The best way to ensure the Abilities API is available for your plugins is to include it as one of your `Requires Plugins` in your [Plugin header](https://developer.wordpress.org/plugins/plugin-basics/header-requirements/). For example:
```diff
# my-plugin.php
/*
*
* Plugin Name: My Plugin
* Plugin URI: https://example.com/plugins/the-basics/
* Description: Handle the basics with this plugin.
* {all the other plugin header fields...}
+ * Requires Plugins: abilities-api
*/
```
While this is enough to ensure the Abilities API is loaded before your plugin, if you need to ensure specific version requirements or provide users guidance on installing the plugin, you can use the methods described [later on](#checking-availability-with-code)
#### As a Composer dependency
```bash
composer require wordpress/abilities-api
```
### Checking availability with code
To ensure the Abilities API is loaded in your plugin:
```php
if ( ! class_exists( 'WP_Ability' ) ) {
// E.g. add an admin notice about the missing dependency.
add_action( 'admin_notices', static function() {
wp_admin_notice(
// If it's a Composer dependency, you might want to suggest running `composer install` instead.
esc_html__( 'This plugin requires the Abilities API to use. Please install and activate it.', 'my-plugin' ),
'error'
);
} );
return;
}
```
You can also check for specific versions of the Abilities API using the `WP_ABILITIES_API_VERSION` constant:
```php
if ( ! defined( 'WP_ABILITIES_API_VERSION' ) || version_compare( WP_ABILITIES_API_VERSION, '0.1.0', '<' ) ) {
// E.g. add an admin notice about the required version.
add_action( 'admin_notices', static function() {
wp_admin_notice(
esc_html__( 'This plugin requires Abilities API version 0.1.0 or higher. Please update the plugin dependency.', 'my-plugin' ),
'error'
);
} );
return;
}
```
## Basic Usage Example
The below example is for a plugin implementation, but it could also be adapted for a theme's functions.php
```php
<?php
// 1. Define a callback function for your ability.
function my_plugin_get_site_title( array $input = array() ): string {
return get_bloginfo( 'name' );
}
// 2. Register the ability when the Abilities API is initialized.
// Using `abilities_api_init` ensures the API is fully loaded.
add_action( 'abilities_api_init', 'my_plugin_register_abilities' );
function my_plugin_register_abilities() {
wp_register_ability( 'my-plugin/get-site-title', array(
'label' => __( 'Get Site Title', 'my-plugin' ),
'description' => __( 'Retrieves the title of the current WordPress site.', 'my-plugin' ),
'input_schema' => array(
'type' => 'object',
'properties' => array(),
'additionalProperties' => false,
),
'output_schema' => array(
'type' => 'string',
'description' => 'The site title.',
),
'execute_callback' => 'my_plugin_get_site_title',
'permission_callback' => '__return_true', // Everyone can access this
'meta' => array(
'category' => 'site-info',
'show_in_rest' => true, // Optional: expose via REST API
) );
}
// 3. Later, you can retrieve and execute the ability.
add_action( 'admin_init', 'my_plugin_use_ability' );
function my_plugin_use_ability() {
$ability = wp_get_ability( 'my-plugin/get-site-title' );
if ( ! $ability ) {
// Ability not found.
return;
}
$site_title = $ability->execute();
if ( is_wp_error( $site_title ) ) {
// Handle execution error
error_log( 'Execution error: ' . $site_title->get_error_message() );
return;
}
// `$site_title` now holds the result of `get_bloginfo( 'name' )`.
echo 'Site Title: ' . esc_html( $site_title );
}
```

View File

@@ -0,0 +1,282 @@
# 3. Registering Abilities (`wp_register_ability`)
The primary way to add functionality to the Abilities API is by using the `wp_register_ability()` function, typically hooked into the `abilities_api_init` action.
## Function Signature
```php
wp_register_ability( string $id, array $args ): ?\WP_Ability
```
- `$id` (`string`): A unique identifier for the ability.
- `$args` (`array`): An array of arguments defining the ability configuration.
- **Return:** (`?\WP_Ability`) An instance of the registered ability if it was successfully registered, `null` on failure (e.g., invalid arguments, duplicate ID).
## Parameters Explained
The `$args` array accepts the following keys:
- `label` (`string`, **Required**): A human-readable name for the ability. Used for display purposes. Should be translatable.
- `description` (`string`, **Required**): A detailed description of what the ability does, its purpose, and its parameters or return values. This is crucial for AI agents to understand how and when to use the ability. Should be translatable.
- `category` (`string`, **Required**): The slug of the category this ability belongs to. The category must be registered before registering the ability using `wp_register_ability_category()`. Categories help organize and filter abilities by their purpose. See [Registering Categories](7.registering-categories.md) for details.
- `input_schema` (`array`, **Required**): A JSON Schema definition describing the expected input parameters for the ability's execute callback. Used for validation and documentation.
- `output_schema` (`array`, **Required**): A JSON Schema definition describing the expected format of the data returned by the ability. Used for validation and documentation.
- `execute_callback` (`callable`, **Required**): The PHP function or method to execute when this ability is called.
- The callback receives one optional argument: it can have any type as defined in the input schema (e.g., `array`, `object`, `string`, etc.).
- The callback should return the result of the ability's operation or return a `WP_Error` object on failure.
- `permission_callback` (`callable`, **Required**): A callback function to check if the current user has permission to execute this ability.
- The callback receives one optional argument: it can have any type as defined in the input schema (e.g., `array`, `object`, `string`, etc.).
- The callback should return a boolean (`true` if the user has permission, `false` otherwise), or a `WP_Error` object on failure.
- If the input does not validate against the input schema, the permission callback will not be called, and a `WP_Error` will be returned instead.
- `meta` (`array`, **Optional**): An associative array for storing arbitrary additional metadata about the ability.
- `annotations` (`array`, **Optional**): An associative array of annotations providing hints about the ability's behavior characteristics. Supports the following keys:
- `instructions` (`string`, **Optional**): Custom instructions or guidance for using the ability (default: `''`).
- `readonly` (`boolean`, **Optional**): Whether the ability only reads data without modifying its environment (default: `false`).
- `destructive` (`boolean`, **Optional**): Whether the ability may perform destructive updates to its environment. If `true`, the ability may perform any type of modification, including deletions or other destructive changes. If `false`, the ability performs only additive updates (default: `true`).
- `idempotent` (`boolean`, **Optional**): Whether calling the ability repeatedly with the same arguments will have no additional effect on its environment (default: `false`).
- `show_in_rest` (`boolean`, **Optional**): Whether to expose this ability via the REST API. Default: `false`.
- When `true`, the ability will be listed in REST API responses and can be executed via REST endpoints.
- When `false`, the ability will be hidden from REST API listings and cannot be executed via REST endpoints, but remains available for internal PHP usage.
## Ability ID Convention
The `$id` parameter must follow the pattern `namespace/ability-name`:
- **Format:** Must contain only lowercase alphanumeric characters (`a-z`, `0-9`), hyphens (`-`), and one forward slash (`/`) for namespacing.
- **Convention:** Use your plugin slug as the namespace, like `my-plugin/ability-name`.
- **Examples:** `my-plugin/update-settings`, `woocommerce/get-product`, `contact-form/send-message`, `analytics/track-event`
## Code Examples
### Registering a Simple Data Retrieval Ability
```php
add_action( 'abilities_api_init', 'my_plugin_register_site_info_ability' );
function my_plugin_register_site_info_ability() {
wp_register_ability( 'my-plugin/get-site-info', array(
'label' => __( 'Get Site Information', 'my-plugin' ),
'description' => __( 'Retrieves basic information about the WordPress site including name, description, and URL.', 'my-plugin' ),
'category' => 'data-retrieval',
'input_schema' => array(
'type' => 'object',
'properties' => array(),
'additionalProperties' => false
),
'output_schema' => array(
'type' => 'object',
'properties' => array(
'name' => array(
'type' => 'string',
'description' => 'Site name'
),
'description' => array(
'type' => 'string',
'description' => 'Site tagline'
),
'url' => array(
'type' => 'string',
'format' => 'uri',
'description' => 'Site URL'
)
)
),
'execute_callback' => function( $input ) {
return array(
'name' => get_bloginfo( 'name' ),
'description' => get_bloginfo( 'description' ),
'url' => home_url()
);
},
'permission_callback' => '__return_true',
'meta' => array(
'annotations' => array(
'readonly' => true,
'destructive' => false
),
),
));
}
```
### Registering an Ability with Input Parameters
```php
add_action( 'abilities_api_init', 'my_plugin_register_update_option_ability' );
function my_plugin_register_update_option_ability() {
wp_register_ability( 'my-plugin/update-option', array(
'label' => __( 'Update WordPress Option', 'my-plugin' ),
'description' => __( 'Updates the value of a WordPress option in the database. Requires manage_options capability.', 'my-plugin' ),
'category' => 'data-modification',
'input_schema' => array(
'type' => 'object',
'properties' => array(
'option_name' => array(
'type' => 'string',
'description' => 'The name of the option to update',
'minLength' => 1
),
'option_value' => array(
'description' => 'The new value for the option'
)
),
'required' => array( 'option_name', 'option_value' ),
'additionalProperties' => false
),
'output_schema' => array(
'type' => 'object',
'properties' => array(
'success' => array(
'type' => 'boolean',
'description' => 'Whether the option was successfully updated'
),
'previous_value' => array(
'description' => 'The previous value of the option'
)
)
),
'execute_callback' => function( $input ) {
$option_name = $input['option_name'];
$new_value = $input['option_value'];
$previous_value = get_option( $option_name );
$success = update_option( $option_name, $new_value );
return array(
'success' => $success,
'previous_value' => $previous_value
);
},
'permission_callback' => function() {
return current_user_can( 'manage_options' );
},
'meta' => array(
'annotations' => array(
'destructive' => false,
'idempotent' => true
),
),
));
}
```
### Registering an Ability with Plugin Dependencies
```php
add_action( 'abilities_api_init', 'my_plugin_register_woo_stats_ability' );
function my_plugin_register_woo_stats_ability() {
// Only register if WooCommerce is active
if ( ! class_exists( 'WooCommerce' ) ) {
return;
}
wp_register_ability( 'my-plugin/get-woo-stats', array(
'label' => __( 'Get WooCommerce Statistics', 'my-plugin' ),
'description' => __( 'Retrieves basic WooCommerce store statistics including total orders and revenue.', 'my-plugin' ),
'category' => 'ecommerce',
'input_schema' => array(
'type' => 'object',
'properties' => array(
'period' => array(
'type' => 'string',
'enum' => array( 'today', 'week', 'month', 'year' ),
'default' => 'month',
'description' => 'Time period for statistics'
)
),
'additionalProperties' => false
),
'output_schema' => array(
'type' => 'object',
'properties' => array(
'total_orders' => array(
'type' => 'integer',
'description' => 'Number of orders in period'
),
'total_revenue' => array(
'type' => 'number',
'description' => 'Total revenue in period'
)
)
),
'execute_callback' => function( $input ) {
$period = $input['period'] ?? 'month';
// Implementation would calculate stats based on period
return array(
'total_orders' => 42,
'total_revenue' => 1250.50
);
},
'permission_callback' => function() {
return current_user_can( 'manage_woocommerce' );
},
'meta' => array(
'requires_plugin' => 'woocommerce',
'category' => 'ecommerce'
)
));
}
```
### Registering an Ability That May Fail
```php
add_action( 'abilities_api_init', 'my_plugin_register_send_email_ability' );
function my_plugin_register_send_email_ability() {
wp_register_ability( 'my-plugin/send-email', array(
'label' => __( 'Send Email', 'my-plugin' ),
'description' => __( 'Sends an email to the specified recipient using WordPress mail functions.', 'my-plugin' ),
'category' => 'communication',
'input_schema' => array(
'type' => 'object',
'properties' => array(
'to' => array(
'type' => 'string',
'format' => 'email',
'description' => 'Recipient email address'
),
'subject' => array(
'type' => 'string',
'minLength' => 1,
'description' => 'Email subject'
),
'message' => array(
'type' => 'string',
'minLength' => 1,
'description' => 'Email message body'
)
),
'required' => array( 'to', 'subject', 'message' ),
'additionalProperties' => false
),
'output_schema' => array(
'type' => 'object',
'properties' => array(
'sent' => array(
'type' => 'boolean',
'description' => 'Whether the email was successfully sent'
)
)
),
'execute_callback' => function( $input ) {
$sent = wp_mail(
$input['to'],
$input['subject'],
$input['message']
);
if ( ! $sent ) {
return new \WP_Error(
'email_send_failed',
sprintf( __( 'Failed to send email' ), 'my-plugin' )
);
}
return array( 'sent' => true );
},
'permission_callback' => function() {
return current_user_can( 'publish_posts' );
}
));
}
```

View File

@@ -0,0 +1,211 @@
# 4. Using Abilities (`wp_get_ability`, `wp_get_abilities`)
Once abilities are registered, they can be retrieved and executed using global functions from the Abilities API.
## Getting a Specific Ability (`wp_get_ability`)
To get a single ability object by its name (namespace/ability-name):
```php
/**
* Retrieves a registered ability using Abilities API.
*
* @param string $name The name of the registered ability, with its namespace.
* @return ?WP_Ability The registered ability instance, or null if it is not registered.
*/
function wp_get_ability( string $name ): ?WP_Ability
// Example:
$site_info_ability = wp_get_ability( 'my-plugin/get-site-info' );
if ( $site_info_ability ) {
// Ability exists and is registered
$site_info = $site_info_ability->execute();
if ( is_wp_error( $site_info ) ) {
// Handle WP_Error
echo 'Error: ' . $site_info->get_error_message();
} else {
// Use $site_info array
echo 'Site Name: ' . $site_info['name'];
}
} else {
// Ability not found or not registered
}
```
## Getting All Registered Abilities (`wp_get_abilities`)
To get an array of all registered abilities:
```php
/**
* Retrieves all registered abilities using Abilities API.
*
* @return WP_Ability[] The array of registered abilities.
*/
function wp_get_abilities(): array
// Example: Get all registered abilities
$all_abilities = wp_get_abilities();
foreach ( $all_abilities as $name => $ability ) {
echo 'Ability Name: ' . esc_html( $ability->get_name() ) . "\n";
echo 'Label: ' . esc_html( $ability->get_label() ) . "\n";
echo 'Description: ' . esc_html( $ability->get_description() ) . "\n";
echo "---\n";
}
```
## Executing an Ability (`$ability->execute()`)
Once you have a `WP_Ability` object (usually from `wp_get_ability`), you execute it using the `execute()` method.
```php
/**
* Executes the ability after input validation and running a permission check.
*
* @param mixed $input Optional. The input data for the ability. Defaults to `null`.
* @return mixed|WP_Error The result of the ability execution, or WP_Error on failure.
*/
// public function execute( $input = null )
// Example 1: Ability with no input parameters
$ability = wp_get_ability( 'my-plugin/get-site-info' );
if ( $ability ) {
$site_info = $ability->execute(); // No input required
if ( is_wp_error( $site_info ) ) {
// Handle WP_Error
echo 'Error: ' . $site_info->get_error_message();
} else {
// Use $site_info array
echo 'Site Name: ' . $site_info['name'];
}
}
// Example 2: Ability with input parameters
$ability = wp_get_ability( 'my-plugin/update-option' );
if ( $ability ) {
$input = array(
'option_name' => 'blogname',
'option_value' => 'My Updated Site Name',
);
$result = $ability->execute( $input );
if ( is_wp_error( $result ) ) {
// Handle WP_Error
echo 'Error: ' . $result->get_error_message();
} else {
// Use $result
if ( $result['success'] ) {
echo 'Option updated successfully!';
echo 'Previous value: ' . $result['previous_value'];
}
}
}
// Example 3: Ability with complex input validation
$ability = wp_get_ability( 'my-plugin/send-email' );
if ( $ability ) {
$input = array(
'to' => 'user@example.com',
'subject' => 'Hello from WordPress',
'message' => 'This is a test message from the Abilities API.',
);
$result = $ability->execute( $input );
if ( is_wp_error( $result ) ) {
// Handle WP_Error
echo 'Error: ' . $result->get_error_message();
} elseif ( $result['sent'] ) {
echo 'Email sent successfully!';
} else {
echo 'Email failed to send.';
}
}
```
## Checking Permissions (`$ability->check_permissions()`)
You can check if the current user has permissions to execute the ability, also without executing it. The `check_permissions()` method returns either `true`, `false`, or a `WP_Error` object. `true` means permission is granted, `false` means the user simply lacks permission, and a `WP_Error` return value typically indicates a failure in the permission check process (such as an internal error or misconfiguration). You must use `is_wp_error()` to handle errors properly and distinguish between permission denial and actual errors:
```php
$ability = wp_get_ability( 'my-plugin/update-option' );
if ( $ability ) {
$input = array(
'option_name' => 'blogname',
'option_value' => 'New Site Name',
);
// Check permission before execution - always use is_wp_error() first
$has_permissions = $ability->check_permissions( $input );
if ( true === $has_permissions ) {
// Permissions granted safe to execute.
echo 'You have permissions to execute this ability.';
} else {
// Don't leak permission errors to unauthenticated users.
if ( is_wp_error( $has_permissions ) ) {
error_log( 'Permissions check failed: ' . $has_permissions->get_error_message() );
}
echo 'You do not have permissions to execute this ability.';
}
}
```
## Inspecting Ability Properties
The `WP_Ability` class provides several getter methods to inspect ability properties:
```php
$ability = wp_get_ability( 'my-plugin/get-site-info' );
if ( $ability ) {
// Basic properties
echo 'Name: ' . $ability->get_name() . "\n";
echo 'Label: ' . $ability->get_label() . "\n";
echo 'Description: ' . $ability->get_description() . "\n";
// Schema information
$input_schema = $ability->get_input_schema();
$output_schema = $ability->get_output_schema();
echo 'Input Schema: ' . json_encode( $input_schema, JSON_PRETTY_PRINT ) . "\n";
echo 'Output Schema: ' . json_encode( $output_schema, JSON_PRETTY_PRINT ) . "\n";
// Metadata
$meta = $ability->get_meta();
if ( ! empty( $meta ) ) {
echo 'Metadata: ' . json_encode( $meta, JSON_PRETTY_PRINT ) . "\n";
}
}
```
## Error Handling Patterns
The Abilities API uses several error handling mechanisms:
```php
$ability = wp_get_ability( 'my-plugin/some-ability' );
if ( ! $ability ) {
// Ability not registered
echo 'Ability not found';
return;
}
$result = $ability->execute( $input );
// Check for WP_Error (validation, permission, or callback errors)
if ( is_wp_error( $result ) ) {
echo 'WP_Error: ' . $result->get_error_message();
return;
}
// Check for null result (permission denied, invalid callback, or validation failure)
if ( is_null( $result ) ) {
echo 'Execution returned null - check permissions and callback validity';
return;
}
// Success - use the result
// Process $result based on the ability's output schema
```

View File

@@ -0,0 +1,345 @@
# 5. REST API Reference
The WordPress Abilities API provides REST endpoints that allow external systems to discover and execute abilities via HTTP requests.
## User access
Access to all Abilities REST API endpoints requires an authenticated user (see the [Authentication](#authentication) section). Access to execute individual Abilities is restricted based on the `permission_callback()` of the Ability.
## Controlling REST API Exposure
By default, registered abilities are **not** exposed via the REST API. You can control whether an individual ability appears in the REST API by using the `show_in_rest` meta when registering the ability:
- `show_in_rest => true`: The ability is listed in REST API responses and can be executed via REST endpoints.
- `show_in_rest => false` (default): The ability is hidden from REST API listings and cannot be executed via REST endpoints. The ability remains available for internal PHP usage via `wp_execute_ability()`.
Abilities with meta `show_in_rest => false` will return a `rest_ability_not_found` error if accessed via REST endpoints.
## Schema
The Abilities API endpoints are available under the `/wp/v2/abilities` namespace.
### Ability Object
Abilities are represented in JSON with the following structure:
```json
{
"name": "my-plugin/get-site-info",
"label": "Get Site Information",
"description": "Retrieves basic information about the WordPress site.",
"category": "site-information",
"output_schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Site name"
},
"url": {
"type": "string",
"format": "uri",
"description": "Site URL"
}
}
},
"meta": {
"annotations": {
"instructions": "",
"readonly": true,
"destructive": false,
"idempotent": false
}
}
}
```
## List Abilities
### Definition
`GET /wp/v2/abilities`
### Arguments
- `page` _(integer)_: Current page of the collection. Default: `1`.
- `per_page` _(integer)_: Maximum number of items to return per page. Default: `50`, Maximum: `100`.
- `category` _(string)_: Filter abilities by category slug.
### Example Request
```bash
curl https://example.com/wp-json/wp/v2/abilities
```
### Example Response
```json
[
{
"name": "my-plugin/get-site-info",
"label": "Get Site Information",
"description": "Retrieves basic information about the WordPress site.",
"category": "site-information",
"output_schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Site name"
},
"url": {
"type": "string",
"format": "uri",
"description": "Site URL"
}
}
},
"meta": {
"annotations": {
"instructions": "",
"readonly": false,
"destructive": true,
"idempotent": false
}
}
}
]
```
## List Categories
### Definition
`GET /wp/v2/abilities/categories`
### Arguments
- `page` _(integer)_: Current page of the collection. Default: `1`.
- `per_page` _(integer)_: Maximum number of items to return per page. Default: `50`, Maximum: `100`.
### Example Request
```bash
curl -u 'USERNAME:APPLICATION_PASSWORD' \
https://example.com/wp-json/wp/v2/abilities/categories
```
### Example Response
```json
[
{
"slug": "data-retrieval",
"label": "Data Retrieval",
"description": "Abilities that retrieve and return data from the WordPress site.",
"meta": {},
"_links": {
"self": [
{
"href": "https://example.com/wp-json/wp/v2/abilities/categories/data-retrieval"
}
],
"collection": [
{
"href": "https://example.com/wp-json/wp/v2/abilities/categories"
}
],
"abilities": [
{
"href": "https://example.com/wp-json/wp/v2/abilities?category=data-retrieval"
}
]
}
}
]
```
## Retrieve a Category
### Definition
`GET /wp/v2/abilities/categories/{slug}`
### Arguments
- `slug` _(string)_: The unique slug of the category.
### Example Request
```bash
curl -u 'USERNAME:APPLICATION_PASSWORD' \
https://example.com/wp-json/wp/v2/abilities/categories/data-retrieval
```
### Example Response
```json
{
"slug": "data-retrieval",
"label": "Data Retrieval",
"description": "Abilities that retrieve and return data from the WordPress site.",
"meta": {},
"_links": {
"self": [
{
"href": "https://example.com/wp-json/wp/v2/abilities/categories/data-retrieval"
}
],
"collection": [
{
"href": "https://example.com/wp-json/wp/v2/abilities/categories"
}
],
"abilities": [
{
"href": "https://example.com/wp-json/wp/v2/abilities?category=data-retrieval"
}
]
}
}
```
## Retrieve an Ability
### Definition
`GET /wp/v2/abilities/(?P<namespace>[a-z0-9-]+)/(?P<ability>[a-z0-9-]+)`
### Arguments
- `namespace` _(string)_: The namespace part of the ability name.
- `ability` _(string)_: The ability name part.
### Example Request
```bash
curl https://example.com/wp-json/wp/v2/abilities/my-plugin/get-site-info
```
### Example Response
```json
{
"name": "my-plugin/get-site-info",
"label": "Get Site Information",
"description": "Retrieves basic information about the WordPress site.",
"category": "site-information",
"output_schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Site name"
},
"url": {
"type": "string",
"format": "uri",
"description": "Site URL"
}
}
},
"meta": {
"annotations": {
"instructions": "",
"readonly": true,
"destructive": false,
"idempotent": false
}
}
}
```
## Execute an Ability
Abilities are executed via the `/run` endpoint. The required HTTP method depends on the ability's `readonly` annotation:
- **Read-only abilities** (`readonly: true`) must use **GET**
- **Regular abilities** (default) must use **POST**
This distinction ensures read-only operations use safe HTTP methods that can be cached and don't modify server state.
### Definition
`GET|POST /wp/v2/abilities/(?P<namespace>[a-z0-9-]+)/(?P<ability>[a-z0-9-]+)/run`
### Arguments
- `namespace` _(string)_: The namespace part of the ability name.
- `ability` _(string)_: The ability name part.
- `input` _(integer|number|boolean|string|array|object|null)_: Optional input data for the ability as defined by its input schema.
- For **GET requests**: pass as `input` query parameter (URL-encoded JSON)
- For **POST requests**: pass in JSON body
### Example Request (Read-only, GET)
```bash
# No input
curl https://example.com/wp-json/wp/v2/abilities/my-plugin/get-site-info/run
# With input (URL-encoded)
curl "https://example.com/wp-json/wp/v2/abilities/my-plugin/get-user-info/run?input=%7B%22user_id%22%3A1%7D"
```
### Example Request (Regular, POST)
```bash
# No input
curl -X POST https://example.com/wp-json/wp/v2/abilities/my-plugin/create-draft/run
# With input
curl -X POST \
-H "Content-Type: application/json" \
-d '{"input":{"option_name":"blogname","option_value":"New Site Name"}}' \
https://example.com/wp-json/wp/v2/abilities/my-plugin/update-option/run
```
### Example Response (Success)
```json
{
"name": "My WordPress Site",
"url": "https://example.com"
}
```
### Example Response (Error)
```json
{
"code": "ability_invalid_permissions",
"message": "Ability \"my-plugin/update-option\" does not have necessary permission.",
"data": {
"status": 403
}
}
```
## Authentication
The Abilities API supports all WordPress REST API authentication methods:
- Cookie authentication (same-origin requests)
- Application passwords (recommended for external access)
- Custom authentication plugins
### Using Application Passwords
```bash
curl -u 'USERNAME:APPLICATION_PASSWORD' \
https://example.com/wp-json/wp/v2/abilities
```
## Error Responses
The API returns standard WordPress REST API error responses with these common codes:
- `ability_missing_input_schema` the ability requires input but none was provided.
- `ability_invalid_input` - input validation failed according to the ability's schema.
- `ability_invalid_permissions` - current user lacks permission to execute the ability.
- `ability_invalid_output` - output validation failed according to the ability's schema.
- `ability_invalid_execute_callback` - the ability's execute callback is not callable.
- `rest_ability_not_found` - the requested ability is not registered.
- `rest_category_not_found` - the requested category is not registered.
- `rest_ability_invalid_method` - the requested HTTP method is not allowed for executing the selected ability (e.g., using GET on a read-only ability, or POST on a regular ability).
- `rest_ability_cannot_execute` - the ability cannot be executed due to insufficient permissions.

View File

@@ -0,0 +1,204 @@
# 6. Hooks
The Abilities API provides [WordPress Action and Filter Hooks](https://developer.wordpress.org/apis/hooks/) that allow developers to monitor and respond to ability execution events.
## Quick Links
- [Actions](#actions)
- [`abilities_api_categories_init`](#abilities_api_categories_init)
- [`before_execute_ability`](#before_execute_ability)
- [`after_execute_ability`](#after_execute_ability)
- [Filters](#filters)
- [`register_ability_args`](#register_ability_args)
- [`register_ability_category_args`](#register_ability_category_args)
## Actions
### `abilities_api_categories_init`
Fires when the category registry is first initialized. This is the proper hook to use when registering categories.
```php
do_action( 'abilities_api_categories_init', $registry );
```
#### Parameters
- `$registry` (`\WP_Abilities_Category_Registry`): The category registry instance.
#### Usage Example
```php
/**
* Register custom ability categories.
*
* @param \WP_Abilities_Category_Registry $registry The category registry instance.
*/
function my_plugin_register_categories( $registry ) {
wp_register_ability_category( 'ecommerce', array(
'label' => __( 'E-commerce', 'my-plugin' ),
'description' => __( 'Abilities related to e-commerce functionality.', 'my-plugin' ),
));
wp_register_ability_category( 'analytics', array(
'label' => __( 'Analytics', 'my-plugin' ),
'description' => __( 'Abilities that provide analytical data and insights.', 'my-plugin' ),
));
}
add_action( 'abilities_api_categories_init', 'my_plugin_register_categories' );
```
### `before_execute_ability`
Fires immediately before an ability gets executed, after permission checks have passed but before the execution callback is called.
```php
do_action( 'before_execute_ability', $ability_name, $input );
```
#### Parameters
- `$ability_name` (`string`): The namespaced name of the ability being executed (e.g., `my-plugin/get-posts`).
- `$input` (`mixed`): The input data passed to the ability.
#### Usage Example
```php
/**
* Log each ability execution attempt.
* @param string $ability_name The name of the ability being executed.
* @param mixed $input The input data passed to the ability.
*/
function log_ability_execution( string $ability_name, $input ) {
error_log( 'About to execute ability: ' . $ability_name );
if ( $input !== null ) {
error_log( 'Input: ' . wp_json_encode( $input ) );
}
}
add_action( 'before_execute_ability', 'log_ability_execution', 10, 2 );
```
### `after_execute_ability`
Fires immediately after an ability has finished executing successfully, after output validation has passed.
```php
do_action( 'after_execute_ability', string $ability_name, $input, $result );
```
#### Parameters
- `$ability_name` (`string`): The namespaced name of the ability that was executed.
- `$input` (`mixed`): The input data that was passed to the ability.
- `$result` (`mixed`): The validated result returned by the ability's execution callback.
#### Usage Example
```php
/**
* Log the result of each ability execution.
*
* @param string $ability_name The name of the executed ability.
* @param mixed $input The input data passed to the ability.
* @param mixed $result The result returned by the ability.
*/
function log_ability_result( string $ability_name, $input, $result ) {
error_log( 'Completed ability: ' . $ability_name );
error_log( 'Result: ' . wp_json_encode( $result ) );
}
add_action( 'after_execute_ability', 'log_ability_result', 10, 3 );
```
## Filters
### `register_ability_args`
Allows modification of an Ability's args before they are validated and used to instantiate the Ability.
```php
$args = apply_filters( 'register_ability_args', array $args, string $ability_name );
```
#### Parameters
- `$args` (`array<string,mixed>`): The arguments used to instantiate the ability. See [wp_register_ability()](./3.registering-abilities.md#wp_register_ability) for the full list of args.
- `$ability_name` (`string`): The namespaced name of the ability being registered (e.g., `my-plugin/get-posts`).
#### Usage Example
```php
/**
* Modify ability args before validation.
*
* @param array<string,mixed> $args The arguments used to instantiate the ability.
* @param string $ability_name The name of the ability, with its namespace.
*
* @return array<string,mixed> The modified ability arguments.
*/
function my_modify_ability_args( array $args, string $ability_name ): array {
// Check if the ability name matches what you're looking for.
if ( 'my-namespace/my-ability' !== $ability_name ) {
return $args;
}
// Modify the args as needed.
$args['label'] = __('My Custom Ability Label');
// You can use the old args to build new ones.
$args['description'] = sprintf(
/* translators: 1: Ability name 2: Previous description */
__('This is a custom description for the ability %s. Previously the description was %s', 'text-domain'),
$ability_name,
$args['description'] ?? 'N/A'
);
// Even if they're callbacks.
$args['permission_callback' ] = static function ( $input = null ) use ( $args, $ability_name ) {
$previous_check = is_callable( $args['permission_callback'] ) ? $args['permission_callback']( $input ) : true;
// If we already failed, no need for stricter checks.
if ( ! $previous_check || is_wp_error( $previous_check ) ) {
return $previous_check;
}
return current_user_can( 'my_custom_ability_cap', $ability_name );
}
return $args;
}
add_filter( 'register_ability_args', 'my_modify_ability_args', 10, 2 );
```
### `register_ability_category_args`
Allows modification of a category's arguments before validation.
```php
$args = apply_filters( 'register_ability_category_args', array $args, string $slug );
```
#### Parameters
- `$args` (`array<string,mixed>`): The arguments used to instantiate the category (label, description).
- `$slug` (`string`): The slug of the category being registered.
#### Usage Example
```php
/**
* Modify category args before validation.
*
* @param array<string,mixed> $args The arguments used to instantiate the category.
* @param string $slug The slug of the category being registered.
*
* @return array<string,mixed> The modified category arguments.
*/
function my_modify_category_args( array $args, string $slug ): array {
if ( 'my-category' === $slug ) {
$args['label'] = __( 'My Custom Label', 'my-plugin' );
$args['description'] = __( 'My custom description for this category.', 'my-plugin' );
}
return $args;
}
add_filter( 'register_ability_category_args', 'my_modify_category_args', 10, 2 );
```

View File

@@ -0,0 +1,298 @@
# 7. JavaScript/TypeScript Client
The JavaScript client provides an interface for discovering and executing WordPress Abilities from the browser.
## Overview
The JavaScript client enables frontend code to interact with the Abilities API system. It can:
- Discover all registered abilities on your WordPress site
- Execute server-side PHP abilities
- Register and execute client-side JavaScript abilities
You can read more about installation and setup in the [package readme](../packages/client/README.md).
## Core API Functions
### `getAbilities( args = {} )`
Returns an array of all registered abilities (both server-side and client-side).
**Parameters:** `args` (object, optional) - Query arguments to filter abilities. Supported arguments:
- `category` (string) - Filter abilities by category slug
**Returns:** `Promise<Array>` - Array of ability objects
**Example:**
```javascript
import { getAbilities } from `@wordpress/abilities`;
const abilities = await getAbilities();
console.log(`Found ${abilities.length} abilities`);
// List all abilities
abilities.forEach(ability => {
console.log(`${ability.name}: ${ability.description}`);
});
// Get abilities in a specific category
const dataAbilities = await getAbilities( { category: 'data-retrieval' } );
console.log( `Found ${ dataAbilities.length } data retrieval abilities` );
```
### getAbility( name )
Retrieves a specific ability by name.
**Parameters:**
- `name` (string) - The ability name (e.g., 'my-plugin/get-posts')
**Returns:** `Promise<Object|null>` - The ability object or null if not found
**Example:**
```javascript
const ability = await getAbility( 'my-plugin/get-site-info' );
if ( ability ) {
console.log( 'Label:', ability.label );
console.log( 'Description:', ability.description );
console.log( 'Input Schema:', ability.input_schema );
}
```
### `getAbilityCategories()`
Returns an array of all registered ability categories.
**Parameters:** None
**Returns:** `Promise<Array>` - Array of category objects
**Example:**
```javascript
const categories = await getAbilityCategories();
console.log( `Found ${ categories.length } categories` );
// List all categories
categories.forEach( ( category ) => {
console.log( `${ category.label }: ${ category.description }` );
} );
```
### `getAbilityCategory( slug )`
Retrieves a specific category by slug.
**Parameters:**
- `slug` (string) - The category slug (e.g., 'data-retrieval')
**Returns:** `Promise<Object|null>` - The category object or null if not found
**Example:**
```javascript
const category = await getAbilityCategory( 'data-retrieval' );
if ( category ) {
console.log( 'Label:', category.label );
console.log( 'Description:', category.description );
}
```
### `executeAbility( name, input = null )`
Executes an ability with the provided input data.
**Parameters:**
- `name` (string) - The ability name
- `input` (any, optional) - Input data for the ability
**Returns:** `Promise<any>` - The ability's output
**Example:**
```javascript
// Execute without input
const siteTitle = await executeAbility( 'my-plugin/get-site-title' );
console.log( 'Site:', siteTitle );
// Execute with input parameters
const posts = await executeAbility( 'my-plugin/get-posts', {
category: 'news',
limit: 5,
} );
posts.forEach( ( post ) => console.log( post.title ) );
```
### `registerAbility( ability )`
Registers a client-side ability that runs in the browser.
**Parameters:**
- `ability` (object) - The ability configuration object
**Returns:** `Promise<void>`
**Example:**
```javascript
// showNotification function
const showNotification = ( message ) => {
new Notification( message );
return { success: true, displayed: message };
};
// Register a notification ability which calls the showNotification function
await registerAbility( {
name: 'my-plugin/show-notification',
label: 'Show Notification',
description: 'Display a notification message to the user',
input_schema: {
type: 'object',
properties: {
message: { type: 'string' },
type: { type: 'string', enum: [ 'success', 'error', 'warning', 'info' ] },
},
required: [ 'message' ],
},
callback: async ( { message, type = 'info' } ) => {
// Show browser notification
if ( ! ( 'Notification' in window ) ) {
alert( 'This browser does not support desktop notification' );
return {
success: false,
error: 'Browser does not support notifications',
};
}
if ( Notification.permission !== 'granted' ) {
Notification.requestPermission().then( ( permission ) => {
if ( permission === 'granted' ) {
return showNotification( message );
}
} );
}
return showNotification( message );
},
permissionCallback: () => {
return !! wp.data.select( 'core' ).getCurrentUser();
},
} );
// Use the registered ability
const result = await executeAbility( 'my-plugin/show-notification', {
message: 'Hello World!',
type: 'success',
} );
```
### `unregisterAbility( name )`
Removes a previously registered client-side ability.
**Parameters:**
- `name` (string) - The ability name to unregister
**Returns:** `void`
**Example:**
```javascript
// Unregister an ability
unregisterAbility( 'my-plugin/old-ability' );
```
### `registerAbilityCategory( slug, args )`
Registers a client-side ability category. This is useful when registering client-side abilities that introduce new categories not defined by the server.
**Parameters:**
- `slug` (string) - The category slug (lowercase alphanumeric with dashes only)
- `args` (object) - Category configuration object
- `label` (string) - Human-readable label for the category
- `description` (string) - Detailed description of the category
- `meta` (object, optional) - Optional metadata about the category
**Returns:** `Promise<void>`
**Example:**
```javascript
// Register a new category
await registerAbilityCategory( 'block-editor', {
label: 'Block Editor',
description: 'Abilities for interacting with the WordPress block editor',
} );
// Register a category with metadata
await registerAbilityCategory( 'custom-category', {
label: 'Custom Category',
description: 'A category for custom abilities',
meta: {
priority: 'high',
icon: 'dashicons-admin-customizer',
},
} );
// Then register abilities using the new category
await registerAbility( {
name: 'my-plugin/insert-block',
label: 'Insert Block',
description: 'Inserts a block into the editor',
category: 'block-editor', // Uses the client-registered category
callback: async ( { blockType } ) => {
// Implementation
return { success: true };
},
} );
```
### `unregisterAbilityCategory( slug )`
Removes a previously registered client-side category.
**Parameters:**
- `slug` (string) - The category slug to unregister
**Returns:** `void`
**Example:**
```javascript
// Unregister a category
unregisterAbilityCategory( 'block-editor' );
```
## Error Handling
All functions return promises that may reject with specific error codes:
```javascript
try {
const result = await executeAbility( 'my-plugin/restricted-action', input );
console.log( 'Success:', result );
} catch ( error ) {
switch ( error.code ) {
case 'ability_permission_denied':
console.error( 'Permission denied:', error.message );
break;
case 'ability_invalid_input':
console.error( 'Invalid input:', error.message );
break;
case 'rest_ability_not_found':
console.error( 'Ability not found:', error.message );
break;
default:
console.error( 'Execution failed:', error.message );
}
}
```

View File

@@ -0,0 +1,61 @@
# 7. Registering Categories
Before registering abilities, you must register at least one category. Categories help organize abilities and make them easier to discover and filter.
## Function Signature
```php
wp_register_ability_category( string $slug, array $args ): ?\WP_Ability_Category
```
**Parameters:**
- `$slug` (`string`): A unique identifier for the category. Must contain only lowercase alphanumeric characters and dashes (no underscores, no uppercase).
- `$args` (`array`): Category configuration with these keys:
- `label` (`string`, **Required**): Human-readable name for the category. Should be translatable.
- `description` (`string`, **Required**): Detailed description of the category's purpose. Should be translatable.
- `meta` (`array`, **Optional**): An associative array for storing arbitrary additional metadata about the category.
**Return:** (`?\WP_Ability_Category`) An instance of the registered category if it was successfully registered, `null` on failure (e.g., invalid arguments, duplicate slug).
**Note:** Categories must be registered during the `abilities_api_categories_init` action hook.
## Code Example
```php
add_action( 'abilities_api_categories_init', 'my_plugin_register_categories' );
function my_plugin_register_categories() {
wp_register_ability_category( 'data-retrieval', array(
'label' => __( 'Data Retrieval', 'my-plugin' ),
'description' => __( 'Abilities that retrieve and return data from the WordPress site.', 'my-plugin' ),
));
wp_register_ability_category( 'data-modification', array(
'label' => __( 'Data Modification', 'my-plugin' ),
'description' => __( 'Abilities that modify data on the WordPress site.', 'my-plugin' ),
));
wp_register_ability_category( 'communication', array(
'label' => __( 'Communication', 'my-plugin' ),
'description' => __( 'Abilities that send messages or notifications.', 'my-plugin' ),
));
}
```
## Category Slug Convention
The `$slug` parameter must follow these rules:
- **Format:** Must contain only lowercase alphanumeric characters (`a-z`, `0-9`) and hyphens (`-`).
- **Valid examples:** `data-retrieval`, `ecommerce`, `site-information`, `user-management`, `category-123`
- **Invalid examples:**
- Uppercase: `Data-Retrieval`, `MyCategory`
- Underscores: `data_retrieval`
- Special characters: `data.retrieval`, `data/retrieval`, `data retrieval`
- Leading/trailing dashes: `-data`, `data-`
- Double dashes: `data--retrieval`
## Other Category Functions
- `wp_unregister_ability_category( string $slug )` - Remove a registered category. Returns the unregistered category instance or `null` on failure.
- `wp_get_ability_category( string $slug )` - Retrieve a specific category by slug. Returns the category instance or `null` if not found.
- `wp_get_ability_categories()` - Get all registered categories as an associative array keyed by slug.

View File

@@ -0,0 +1,60 @@
# WordPress Ability API Scripts
This directory contains automation scripts for scaffolding and validating WordPress Abilities and Categories.
## Scripts
### Ability Scripts
- **scaffold-ability.php** - Generate ability code from CLI arguments
- **validate-ability.php** - Validate PHP ability registration code
### Category Scripts
- **scaffold-category.php** - Generate category registration code (PHP or JS)
- **validate-category.php** - Validate PHP category registration code
- **validate-category.js** - Validate JavaScript category registration code
## Requirements
### PHP Scripts
- PHP 7.0+ (uses `token_get_all()` for parsing)
- No external dependencies
### JavaScript Validator
- Node.js
- acorn parser
To install acorn:
```bash
npm install acorn
```
Or install globally:
```bash
npm install -g acorn
```
## Usage
See individual script headers for detailed usage instructions.
Quick examples:
```bash
# Scaffold a server-side ability
php scaffold-ability.php --name="plugin/ability" --type="server" --category="data-retrieval"
# Scaffold a category
php scaffold-category.php --name="custom-category" --label="Custom" --description="Description" --type="server"
# Validate PHP category
php validate-category.php path/to/category.php
# Validate JavaScript category
node validate-category.js path/to/category.js
```

View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"acorn": "^8.15.0"
}
}

View File

@@ -0,0 +1,141 @@
#!/usr/bin/env php
<?php
/**
* WordPress Ability Scaffolding Script
*
* Generates ability registration code from templates based on command-line arguments.
* Designed to be called by AI agents to quickly scaffold abilities.
*
* Usage:
* php scaffold-ability.php --name="plugin/ability" --type="server" --category="data-retrieval"
*
* Required Arguments:
* --name Ability name in format "namespace/ability-name"
* --type Type of ability: "server" or "client"
* --category Category slug (e.g., "data-retrieval", "data-modification")
*
* Optional Arguments:
* --label Human-readable label (default: generated from name)
* --description Detailed description (default: placeholder)
* --readonly Whether ability only reads data: "true" or "false" (default: false)
* --destructive Whether ability can delete data: "true" or "false" (default: true)
* --idempotent Whether repeated calls are safe: "true" or "false" (default: false)
*
* Output:
* Complete ability registration code printed to stdout
*
* Exit Codes:
* 0 - Success
* 1 - Missing required arguments
* 2 - Invalid argument values
*/
// Parse command-line arguments
$options = getopt('', [
'name:',
'type:',
'category:',
'label::',
'description::',
'readonly::',
'destructive::',
'idempotent::',
]);
// Validate required arguments
$required = ['name', 'type', 'category'];
$missing = array_diff($required, array_keys($options));
if (!empty($missing)) {
fwrite(STDERR, "Error: Missing required arguments: " . implode(', ', $missing) . "\n");
fwrite(STDERR, "Usage: php scaffold-ability.php --name=\"plugin/ability\" --type=\"server\" --category=\"data-retrieval\"\n");
exit(1);
}
// Extract and validate arguments
$name = $options['name'];
$type = strtolower($options['type']);
$category = $options['category'];
// Validate ability name format
if (!preg_match('/^[a-z0-9-]+\/[a-z0-9-]+$/', $name)) {
fwrite(STDERR, "Error: Invalid ability name format. Must be 'namespace/ability-name' (lowercase, hyphens allowed)\n");
exit(2);
}
// Validate type
if (!in_array($type, ['server', 'client'])) {
fwrite(STDERR, "Error: Invalid type. Must be 'server' or 'client'\n");
exit(2);
}
// Parse optional arguments with defaults
$label = $options['label'] ?? generate_label_from_name($name);
$description = $options['description'] ?? 'TODO: Add a detailed description of what this ability does.';
$readonly = parse_bool($options['readonly'] ?? 'false');
$destructive = parse_bool($options['destructive'] ?? 'true');
$idempotent = parse_bool($options['idempotent'] ?? 'false');
// Load the appropriate template and generate code
$template_file = $type === 'server'
? __DIR__ . '/../assets/server-ability-template.php'
: __DIR__ . '/../assets/client-ability-template.js';
if (!file_exists($template_file)) {
fwrite(STDERR, "Error: Template file not found: {$template_file}\n");
exit(2);
}
$template = file_get_contents($template_file);
if ($template === false) {
fwrite(STDERR, "Error: Unable to read template file: {$template_file}\n");
exit(2);
}
// Prepare placeholder replacements
$namespace = explode('/', $name)[0];
$ability_slug = str_replace(['/', '-'], '_', $name);
$placeholders = [
'{{ABILITY_NAME}}' => $name,
'{{NAMESPACE}}' => $namespace,
'{{LABEL}}' => $label,
'{{DESCRIPTION}}' => $description,
'{{CATEGORY}}' => $category,
'{{CALLBACK_FUNCTION}}' => $ability_slug . '_callback',
'{{REGISTER_FUNCTION}}' => $ability_slug . '_register',
'{{READONLY}}' => $readonly ? 'true' : 'false',
'{{DESTRUCTIVE}}' => $destructive ? 'true' : 'false',
'{{IDEMPOTENT}}' => $idempotent ? 'true' : 'false',
];
// Replace placeholders
$output = str_replace(array_keys($placeholders), array_values($placeholders), $template);
// Output the result
echo $output;
exit(0);
/**
* Generate a human-readable label from the ability name.
*
* @param string $name Ability name in format "namespace/ability-name"
* @return string Human-readable label
*/
function generate_label_from_name($name) {
$parts = explode('/', $name);
$ability_part = end($parts);
$words = explode('-', $ability_part);
return ucwords(implode(' ', $words));
}
/**
* Parse boolean string to actual boolean.
*
* @param string $value String value ("true" or "false")
* @return bool
*/
function parse_bool($value) {
return strtolower($value) === 'true';
}

View File

@@ -0,0 +1,97 @@
#!/usr/bin/env php
<?php
/**
* WordPress Ability Category Scaffolding Script
*
* Generates category registration code from templates based on command-line arguments.
* Designed to be called by AI agents to quickly scaffold categories.
*
* Usage:
* php scaffold-category.php --name="category-slug" --label="Category Label" --description="Description" --type="server"
*
* Required Arguments:
* --name Category slug (kebab-case, e.g., "data-retrieval")
* --label Human-readable label
* --description Detailed description of what abilities belong in this category
* --type Type: "server" (PHP) or "client" (JavaScript)
*
* Output:
* Complete category registration code printed to stdout
*
* Exit Codes:
* 0 - Success
* 1 - Missing required arguments
* 2 - Invalid argument values
*/
// Parse command-line arguments
$options = getopt('', [
'name:',
'label:',
'description:',
'type:',
]);
// Validate required arguments
$required = ['name', 'label', 'description', 'type'];
$missing = array_diff($required, array_keys($options));
if (!empty($missing)) {
fwrite(STDERR, "Error: Missing required arguments: " . implode(', ', $missing) . "\n");
fwrite(STDERR, "Usage: php scaffold-category.php --name=\"category-slug\" --label=\"Label\" --description=\"Description\" --type=\"server\"\n");
exit(1);
}
// Extract and validate arguments
$name = $options['name'];
$label = $options['label'];
$description = $options['description'];
$type = strtolower($options['type']);
// Validate category name format (kebab-case)
if (!preg_match('/^[a-z0-9-]+$/', $name)) {
fwrite(STDERR, "Error: Invalid category name format. Must be kebab-case (lowercase, hyphens allowed, e.g., 'data-retrieval')\n");
exit(2);
}
// Validate type
if (!in_array($type, ['server', 'client'])) {
fwrite(STDERR, "Error: Invalid type. Must be 'server' or 'client'\n");
exit(2);
}
// Load the appropriate template
$template_file = $type === 'server'
? __DIR__ . '/../assets/category-template.php'
: __DIR__ . '/../assets/client-category-template.js';
if (!file_exists($template_file)) {
fwrite(STDERR, "Error: Template file not found: {$template_file}\n");
exit(2);
}
$template = file_get_contents($template_file);
if ($template === false) {
fwrite(STDERR, "Error: Unable to read template file: {$template_file}\n");
exit(2);
}
// Prepare placeholder replacements
$register_function = str_replace('-', '_', $name) . '_category_register';
$namespace = 'text-domain'; // Generic text domain for templates
$placeholders = [
'{{CATEGORY_NAME}}' => $name,
'{{LABEL}}' => $label,
'{{DESCRIPTION}}' => $description,
'{{REGISTER_FUNCTION}}' => $register_function,
'{{NAMESPACE}}' => $namespace,
];
// Replace placeholders
$output = str_replace(array_keys($placeholders), array_values($placeholders), $template);
// Output the result
echo $output;
exit(0);

View File

@@ -0,0 +1,439 @@
#!/usr/bin/env node
/**
* WordPress Ability Validation Script (JavaScript)
*
* Validates JavaScript ability registration code using acorn AST parser.
* For PHP validation, use validate-ability.php instead.
*
* Usage:
* node validate-ability.js path/to/ability-file.js
*
* Requirements:
* - Node.js
* - acorn parser: npm install acorn
*
* Arguments:
* file_path Path to the JavaScript file containing ability registration
*
* Exit Codes:
* 0 - Validation passed
* 1 - Validation failed
* 2 - File not found, invalid usage, or missing dependencies
*/
const fs = require('fs');
const path = require('path');
// Check for acorn dependency
let acorn;
try {
acorn = require('acorn');
} catch (e) {
console.error(
'Error: acorn parser not found. Install with: npm install acorn'
);
process.exit(2);
}
// Check for file argument
if (process.argv.length < 3) {
console.error('Usage: node validate-ability.js path/to/ability-file.js');
process.exit(2);
}
const filePath = process.argv[2];
// Check if file exists
if (!fs.existsSync(filePath)) {
console.error(`Error: File not found: ${filePath}`);
process.exit(2);
}
// Check if file is JavaScript
const ext = path.extname(filePath);
if (!['.js', '.jsx', '.ts', '.tsx', '.mjs'].includes(ext)) {
console.error(
'Error: This validator only supports JavaScript files. For PHP validation, use validate-ability.php'
);
process.exit(2);
}
// Read file contents
let content;
try {
content = fs.readFileSync(filePath, 'utf8');
} catch (e) {
console.error(`Error: Unable to read file: ${filePath}`);
process.exit(2);
}
// Initialize validation results
const errors = [];
const warnings = [];
// Parse and validate JavaScript abilities
try {
const ast = acorn.parse(content, {
sourceType: 'module',
ecmaVersion: 'latest',
allowAwaitOutsideFunction: true,
});
const abilities = extractAbilities(ast);
if (abilities.length === 0) {
errors.push('No registerAbility() calls found in file');
} else {
abilities.forEach((ability) => {
console.log(`Validating ability: ${ability.name || '(unnamed)'}`);
validateAbility(ability, errors, warnings);
console.log('');
});
}
} catch (e) {
if (e instanceof SyntaxError) {
errors.push(`JavaScript syntax error: ${e.message}`);
} else {
throw e;
}
}
// Output final results
outputResults(filePath, errors, warnings);
// Exit with appropriate code
process.exit(errors.length > 0 ? 1 : 0);
/**
* Extract ability registrations from AST.
*
* @param {Object} ast - Acorn AST
* @returns {Array} Array of ability objects
*/
function extractAbilities(ast) {
const abilities = [];
const processed = new Set(); // Track processed nodes to avoid duplicates
// Walk the AST to find registerAbility calls
walk(ast, (node) => {
let callNode = null;
// Direct call: registerAbility()
if (
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === 'registerAbility' &&
!processed.has(node)
) {
callNode = node;
}
// Await call: await registerAbility()
if (
node.type === 'AwaitExpression' &&
node.argument.type === 'CallExpression' &&
node.argument.callee.type === 'Identifier' &&
node.argument.callee.name === 'registerAbility' &&
!processed.has(node.argument)
) {
callNode = node.argument;
}
if (callNode) {
processed.add(callNode);
const ability = extractAbilityFromCall(callNode);
if (ability) {
abilities.push(ability);
}
}
});
return abilities;
}
/**
* Extract ability data from a CallExpression node.
*
* @param {Object} node - CallExpression AST node
* @returns {Object|null} Ability object or null
*/
function extractAbilityFromCall(node) {
if (node.arguments.length < 1) {
return null;
}
const ability = {
name: null,
label: null,
description: null,
category: null,
hasCallback: false,
hasPermissionCallback: false,
hasInputSchema: false,
hasOutputSchema: false,
annotations: {},
};
// First argument: config object
const configArg = node.arguments[0];
if (configArg.type !== 'ObjectExpression') {
return null; // Config must be an object
}
// Extract properties from config object
configArg.properties.forEach((prop) => {
if (prop.type !== 'Property' || prop.key.type !== 'Identifier') {
return;
}
const key = prop.key.name;
// Extract string fields
if (['name', 'label', 'description', 'category'].includes(key)) {
if (
prop.value.type === 'Literal' &&
typeof prop.value.value === 'string'
) {
ability[key] = prop.value.value;
}
}
// Check for callback (function or arrow function)
if (key === 'callback') {
ability.hasCallback = [
'FunctionExpression',
'ArrowFunctionExpression',
].includes(prop.value.type);
}
// Check for permissionCallback
if (key === 'permissionCallback') {
ability.hasPermissionCallback = [
'FunctionExpression',
'ArrowFunctionExpression',
].includes(prop.value.type);
}
// Check for schemas (object expressions)
if (key === 'inputSchema' && prop.value.type === 'ObjectExpression') {
ability.hasInputSchema = true;
}
if (key === 'outputSchema' && prop.value.type === 'ObjectExpression') {
ability.hasOutputSchema = true;
}
// Extract annotations from meta
if (key === 'meta' && prop.value.type === 'ObjectExpression') {
prop.value.properties.forEach((metaProp) => {
if (
metaProp.type === 'Property' &&
metaProp.key.type === 'Identifier' &&
metaProp.key.name === 'annotations' &&
metaProp.value.type === 'ObjectExpression'
) {
metaProp.value.properties.forEach((annoProp) => {
if (
annoProp.type === 'Property' &&
annoProp.key.type === 'Identifier'
) {
const annoKey = annoProp.key.name;
if (
[
'readonly',
'destructive',
'idempotent',
].includes(annoKey)
) {
if (
annoProp.value.type === 'Literal' &&
typeof annoProp.value.value === 'boolean'
) {
ability.annotations[annoKey] =
annoProp.value.value;
}
}
}
});
}
});
}
});
return ability;
}
/**
* Validate ability data.
*
* @param {Object} ability - Ability data
* @param {Array} errors - Error messages array
* @param {Array} warnings - Warning messages array
*/
function validateAbility(ability, errors, warnings) {
const {
name,
label,
description,
category,
hasCallback,
hasPermissionCallback,
hasInputSchema,
hasOutputSchema,
annotations,
} = ability;
// Validate required fields
if (!name) {
errors.push('Missing required field: name');
return; // Can't continue without a name
}
// Validate name format (namespace/ability-name in kebab-case)
if (!/^[a-z0-9-]+\/[a-z0-9-]+$/.test(name)) {
errors.push(
`Invalid ability name format: '${name}'. Must be 'namespace/ability-name' in kebab-case (e.g., 'my-plugin/do-something')`
);
}
if (!label) {
errors.push(`Missing required field 'label' in ability '${name}'`);
} else {
// Validate label quality
if (label.trim().length < 2) {
warnings.push(
`Label for ability '${name}' is too short. Provide a meaningful label.`
);
}
}
if (!description) {
errors.push(
`Missing required field 'description' in ability '${name}'`
);
} else {
// Validate description quality
const descLength = description.trim().length;
if (descLength < 20) {
warnings.push(
`Description for ability '${name}' is too short (${descLength} chars). Provide detailed information about what this ability does, when to use it, and what parameters it accepts.`
);
}
if (/TODO/i.test(description)) {
warnings.push(
`Description for ability '${name}' contains TODO placeholder. Replace with actual description.`
);
}
}
if (!category) {
errors.push(`Missing required field 'category' in ability '${name}'`);
}
// Validate callback presence
if (!hasCallback) {
errors.push(
`Missing required field 'callback' in ability '${name}'. Abilities must have an execute callback function.`
);
}
// Validate permission callback
if (!hasPermissionCallback) {
warnings.push(
`Missing 'permissionCallback' in ability '${name}'. Consider adding permission checks to control who can execute this ability.`
);
}
// Validate schemas (recommended if ability takes input or provides output)
if (!hasInputSchema) {
warnings.push(
`Missing 'inputSchema' in ability '${name}'. Input schema should be provided if this ability accepts parameters.`
);
}
if (!hasOutputSchema) {
warnings.push(
`Missing 'outputSchema' in ability '${name}'. Output schema should be provided if this ability returns data.`
);
}
// Validate annotations
if ('readonly' in annotations && 'destructive' in annotations) {
if (annotations.readonly === true && annotations.destructive === true) {
warnings.push(
`Conflicting annotations in ability '${name}': readonly=true and destructive=true. A readonly ability should have destructive=false.`
);
}
}
}
/**
* Simple AST walker.
*
* @param {Object} node - AST node
* @param {Function} callback - Function to call for each node
*/
function walk(node, callback) {
if (!node || typeof node !== 'object') {
return;
}
callback(node);
for (const key in node) {
if (
key === 'loc' ||
key === 'range' ||
key === 'start' ||
key === 'end'
) {
continue; // Skip position metadata
}
const value = node[key];
if (Array.isArray(value)) {
value.forEach((child) => walk(child, callback));
} else if (value && typeof value === 'object') {
walk(value, callback);
}
}
}
/**
* Output validation results.
*
* @param {string} filePath - File being validated
* @param {Array} errors - Error messages
* @param {Array} warnings - Warning messages
*/
function outputResults(filePath, errors, warnings) {
console.log('='.repeat(70));
console.log(`Validation Results: ${filePath}`);
console.log('='.repeat(70));
console.log('');
if (errors.length > 0) {
console.log(`ERRORS (${errors.length}):`);
errors.forEach((error) => console.log(`${error}`));
console.log('');
}
if (warnings.length > 0) {
console.log(`WARNINGS (${warnings.length}):`);
warnings.forEach((warning) => console.log(`${warning}`));
console.log('');
}
if (errors.length === 0 && warnings.length === 0) {
console.log('✓ All validations passed! No issues found.');
console.log('');
} else if (errors.length === 0) {
console.log('✓ Validation passed with warnings.');
console.log('');
} else {
console.log('✗ Validation failed. Please fix the errors above.');
console.log('');
}
}

View File

@@ -0,0 +1,306 @@
#!/usr/bin/env php
<?php
/**
* WordPress Ability Validation Script
*
* Validates ability registration code independently of WordPress.
* Checks structure, required fields, and JSON Schema validity.
*
* Usage:
* php validate-ability.php path/to/ability-file.php
*
* Arguments:
* file_path Path to the PHP file containing ability registration code
*
* Exit Codes:
* 0 - Validation passed
* 1 - Validation failed
* 2 - File not found or invalid usage
*/
// Check for file argument
if ($argc < 2) {
fwrite(STDERR, "Usage: php validate-ability.php path/to/ability-file.php\n");
exit(2);
}
$file_path = $argv[1];
// Check if file exists
if (!file_exists($file_path)) {
fwrite(STDERR, "Error: File not found: {$file_path}\n");
exit(2);
}
// Read file contents
$content = file_get_contents($file_path);
if ($content === false) {
fwrite(STDERR, "Error: Unable to read file: {$file_path}\n");
exit(2);
}
// Initialize validation results
$errors = [];
$warnings = [];
// Extract wp_register_ability calls
$pattern = '/wp_register_ability\s*\(\s*[\'"]([^\'"]+)[\'"]\s*,\s*array\s*\((.*?)\)\s*\)\s*;/s';
preg_match_all($pattern, $content, $matches, PREG_SET_ORDER);
if (empty($matches)) {
$errors[] = "No wp_register_ability() calls found in file";
output_results($file_path, $errors, $warnings);
exit(1);
}
// Validate each ability registration
foreach ($matches as $index => $match) {
$ability_name = $match[1];
$args_string = $match[2];
echo "Validating ability: {$ability_name}\n";
// Validate ability name format
if (!preg_match('/^[a-z0-9-]+\/[a-z0-9-]+$/', $ability_name)) {
$errors[] = "Invalid ability name format: '{$ability_name}'. Must be 'namespace/ability-name' (lowercase, hyphens allowed)";
}
// Parse the arguments array
$args = parse_ability_args($args_string);
// Validate required fields
validate_required_fields($ability_name, $args, $errors, $warnings);
// Validate schemas
if (isset($args['input_schema'])) {
validate_json_schema($ability_name, 'input_schema', $args['input_schema'], $errors);
}
if (isset($args['output_schema'])) {
validate_json_schema($ability_name, 'output_schema', $args['output_schema'], $errors);
}
// Best practice checks
check_best_practices($ability_name, $args, $warnings);
echo "\n";
}
// Output final results
output_results($file_path, $errors, $warnings);
// Exit with appropriate code
exit(empty($errors) ? 0 : 1);
/**
* Parse ability registration arguments from string.
*
* This is a simplified parser that extracts key configuration values.
*
* @param string $args_string The arguments array as a string
* @return array Parsed arguments
*/
function parse_ability_args($args_string) {
$args = [];
// Extract simple string/boolean values
$simple_patterns = [
'label' => '/[\'"]label[\'"]\s*=>\s*(?:__\s*\(\s*)?[\'"]([^\'"]+)[\'"]/',
'description' => '/[\'"]description[\'"]\s*=>\s*(?:__\s*\(\s*)?[\'"]([^\'"]+)[\'"]/',
'category' => '/[\'"]category[\'"]\s*=>\s*[\'"]([^\'"]+)[\'"]/',
'execute_callback' => '/[\'"]execute_callback[\'"]\s*=>\s*[\'"]([^\'"]+)[\'"]/',
];
foreach ($simple_patterns as $key => $pattern) {
if (preg_match($pattern, $args_string, $matches)) {
$args[$key] = $matches[1];
}
}
// Check for permission_callback (function or string)
if (preg_match('/[\'"]permission_callback[\'"]\s*=>\s*(.+?),?\s*(?=[\'"][a-z_]+[\'"]|$)/s', $args_string, $matches)) {
$args['permission_callback'] = trim($matches[1]);
}
// Extract schema arrays (look for 'input_schema' and 'output_schema')
if (preg_match('/[\'"]input_schema[\'"]\s*=>\s*array\s*\((.*?)\),?\s*(?=[\'"][a-z_]+[\'"]|$)/s', $args_string, $matches)) {
$args['input_schema'] = $matches[1];
}
if (preg_match('/[\'"]output_schema[\'"]\s*=>\s*array\s*\((.*?)\),?\s*(?=[\'"][a-z_]+[\'"]|$)/s', $args_string, $matches)) {
$args['output_schema'] = $matches[1];
}
// Extract meta array
if (preg_match('/[\'"]meta[\'"]\s*=>\s*array\s*\((.*?)\),?\s*(?:\)|$)/s', $args_string, $matches)) {
$args['meta'] = $matches[1];
}
return $args;
}
/**
* Validate required fields are present.
*
* @param string $ability_name Ability name
* @param array $args Parsed arguments
* @param array &$errors Error messages array
* @param array &$warnings Warning messages array
*/
function validate_required_fields($ability_name, $args, &$errors, &$warnings) {
$required_fields = ['label', 'description', 'category', 'execute_callback'];
foreach ($required_fields as $field) {
if (!isset($args[$field])) {
$errors[] = "Missing required field '{$field}' in ability '{$ability_name}'";
}
}
// Check for empty descriptions (common mistake)
if (isset($args['description']) && strlen(trim($args['description'])) < 10) {
$warnings[] = "Description for '{$ability_name}' is too short. Provide detailed information for AI agents.";
}
// Check for placeholder/TODO descriptions
if (isset($args['description']) && stripos($args['description'], 'TODO') !== false) {
$warnings[] = "Description for '{$ability_name}' contains TODO placeholder. Replace with actual description.";
}
// Warn about missing schemas (recommended if ability takes input or provides output)
if (!isset($args['input_schema'])) {
$warnings[] = "Missing 'input_schema' in ability '{$ability_name}'. Input schema should be provided if this ability accepts parameters.";
}
if (!isset($args['output_schema'])) {
$warnings[] = "Missing 'output_schema' in ability '{$ability_name}'. Output schema should be provided if this ability returns data.";
}
// Warn about missing permission callback
if (!isset($args['permission_callback'])) {
$warnings[] = "Missing 'permission_callback' in ability '{$ability_name}'. Consider adding permission checks to control who can execute this ability.";
}
}
/**
* Validate a JSON Schema structure.
*
* @param string $ability_name Ability name
* @param string $schema_type Schema type ('input_schema' or 'output_schema')
* @param string $schema_string Schema as string
* @param array &$errors Error messages array
*/
function validate_json_schema($ability_name, $schema_type, $schema_string, &$errors) {
// Check for 'type' field (required in JSON Schema)
if (strpos($schema_string, "'type'") === false && strpos($schema_string, '"type"') === false) {
$errors[] = "{$schema_type} for '{$ability_name}' missing 'type' field (required by JSON Schema)";
}
// Check for 'properties' if type is 'object'
if (preg_match('/[\'"]type[\'"]\s*=>\s*[\'"]object[\'"]/', $schema_string)) {
if (strpos($schema_string, "'properties'") === false && strpos($schema_string, '"properties"') === false) {
$errors[] = "{$schema_type} for '{$ability_name}' has type 'object' but missing 'properties' field";
}
}
// Check for 'items' if type is 'array'
if (preg_match('/[\'"]type[\'"]\s*=>\s*[\'"]array[\'"]/', $schema_string)) {
if (strpos($schema_string, "'items'") === false && strpos($schema_string, '"items"') === false) {
$warnings[] = "{$schema_type} for '{$ability_name}' has type 'array' but missing 'items' field (recommended)";
}
}
// Validate that properties have types
if (preg_match_all('/[\'"]properties[\'"]\s*=>\s*array\s*\((.*?)\)/s', $schema_string, $prop_matches)) {
foreach ($prop_matches[1] as $properties) {
// Extract each property definition
if (preg_match_all('/[\'"]([a-z_]+)[\'"]\s*=>\s*array\s*\((.*?)\)/s', $properties, $prop_defs, PREG_SET_ORDER)) {
foreach ($prop_defs as $prop_def) {
$prop_name = $prop_def[1];
$prop_content = $prop_def[2];
// Each property should have a 'type'
if (strpos($prop_content, "'type'") === false && strpos($prop_content, '"type"') === false) {
$errors[] = "{$schema_type} property '{$prop_name}' in '{$ability_name}' missing 'type' field";
}
}
}
}
}
}
/**
* Check for best practices.
*
* @param string $ability_name Ability name
* @param array $args Parsed arguments
* @param array &$warnings Warning messages array
*/
function check_best_practices($ability_name, $args, &$warnings) {
// Check for __return_true permission callback (context-aware security check)
if (isset($args['permission_callback']) && strpos($args['permission_callback'], '__return_true') !== false) {
// Extract annotations to determine if this is a safe public ability
$is_readonly = false;
$is_destructive = true;
if (isset($args['meta']) && strpos($args['meta'], 'annotations') !== false) {
$is_readonly = preg_match('/[\'"]readonly[\'"]\s*=>\s*true/', $args['meta']);
$is_destructive = !preg_match('/[\'"]destructive[\'"]\s*=>\s*false/', $args['meta']);
}
// Only warn if this is potentially dangerous (not readonly OR is destructive)
if (!$is_readonly || $is_destructive) {
$warnings[] = "Ability '{$ability_name}' uses __return_true() for permissions with write/destructive operations. Ensure public access is intentional.";
}
// If it's readonly and non-destructive, __return_true is fine - no warning needed
}
// Check for annotations in meta
if (isset($args['meta'])) {
$has_annotations = strpos($args['meta'], 'annotations') !== false;
if (!$has_annotations) {
$warnings[] = "Ability '{$ability_name}' missing annotations (readonly, destructive, idempotent). These help AI agents understand the ability's behavior.";
}
} else {
$warnings[] = "Ability '{$ability_name}' missing 'meta' array. Consider adding annotations for better discoverability.";
}
// Check category naming (should be kebab-case)
if (isset($args['category']) && !preg_match('/^[a-z0-9-]+$/', $args['category'])) {
$warnings[] = "Category '{$args['category']}' should use kebab-case naming (lowercase with hyphens)";
}
}
/**
* Output validation results.
*
* @param string $file_path File being validated
* @param array $errors Error messages
* @param array $warnings Warning messages
*/
function output_results($file_path, $errors, $warnings) {
echo str_repeat('=', 70) . "\n";
echo "Validation Results: {$file_path}\n";
echo str_repeat('=', 70) . "\n\n";
if (!empty($errors)) {
echo "ERRORS (" . count($errors) . "):\n";
foreach ($errors as $error) {
echo "{$error}\n";
}
echo "\n";
}
if (!empty($warnings)) {
echo "WARNINGS (" . count($warnings) . "):\n";
foreach ($warnings as $warning) {
echo "{$warning}\n";
}
echo "\n";
}
if (empty($errors) && empty($warnings)) {
echo "✓ All validations passed! No issues found.\n\n";
} elseif (empty($errors)) {
echo "✓ Validation passed with warnings.\n\n";
} else {
echo "✗ Validation failed. Please fix the errors above.\n\n";
}
}

View File

@@ -0,0 +1,317 @@
#!/usr/bin/env node
/**
* WordPress Ability Category Validation Script (JavaScript)
*
* Validates JavaScript category registration code using acorn AST parser.
* For PHP validation, use validate-category.php instead.
*
* Usage:
* node validate-category.js path/to/category-file.js
*
* Requirements:
* - Node.js
* - acorn parser: npm install acorn
*
* Arguments:
* file_path Path to the JavaScript file containing category registration
*
* Exit Codes:
* 0 - Validation passed
* 1 - Validation failed
* 2 - File not found, invalid usage, or missing dependencies
*/
const fs = require('fs');
const path = require('path');
// Check for acorn dependency
let acorn;
try {
acorn = require('acorn');
} catch (e) {
console.error(
'Error: acorn parser not found. Install with: npm install acorn'
);
process.exit(2);
}
// Check for file argument
if (process.argv.length < 3) {
console.error('Usage: node validate-category.js path/to/category-file.js');
process.exit(2);
}
const filePath = process.argv[2];
// Check if file exists
if (!fs.existsSync(filePath)) {
console.error(`Error: File not found: ${filePath}`);
process.exit(2);
}
// Check if file is JavaScript
const ext = path.extname(filePath);
if (!['.js', '.jsx', '.ts', '.tsx', '.mjs'].includes(ext)) {
console.error(
'Error: This validator only supports JavaScript files. For PHP validation, use validate-category.php'
);
process.exit(2);
}
// Read file contents
let content;
try {
content = fs.readFileSync(filePath, 'utf8');
} catch (e) {
console.error(`Error: Unable to read file: ${filePath}`);
process.exit(2);
}
// Initialize validation results
const errors = [];
const warnings = [];
// Parse and validate JavaScript categories
try {
const ast = acorn.parse(content, {
sourceType: 'module',
ecmaVersion: 'latest',
allowAwaitOutsideFunction: true,
});
const categories = extractCategories(ast);
if (categories.length === 0) {
errors.push('No registerAbilityCategory() calls found in file');
} else {
categories.forEach((category) => {
console.log(`Validating category: ${category.name}`);
validateCategory(category, errors, warnings);
console.log('');
});
}
} catch (e) {
if (e instanceof SyntaxError) {
errors.push(`JavaScript syntax error: ${e.message}`);
} else {
throw e;
}
}
// Output final results
outputResults(filePath, errors, warnings);
// Exit with appropriate code
process.exit(errors.length > 0 ? 1 : 0);
/**
* Extract category registrations from AST.
*
* @param {Object} ast - Acorn AST
* @returns {Array} Array of category objects
*/
function extractCategories(ast) {
const categories = [];
const processed = new Set(); // Track processed nodes to avoid duplicates
// Walk the AST to find registerAbilityCategory calls
walk(ast, (node) => {
let callNode = null;
// Direct call: registerAbilityCategory()
if (
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === 'registerAbilityCategory' &&
!processed.has(node)
) {
callNode = node;
}
// Await call: await registerAbilityCategory()
if (
node.type === 'AwaitExpression' &&
node.argument.type === 'CallExpression' &&
node.argument.callee.type === 'Identifier' &&
node.argument.callee.name === 'registerAbilityCategory' &&
!processed.has(node.argument)
) {
callNode = node.argument;
}
if (callNode) {
processed.add(callNode);
const category = extractCategoryFromCall(callNode);
if (category) {
categories.push(category);
}
}
});
return categories;
}
/**
* Extract category data from a CallExpression node.
*
* @param {Object} node - CallExpression AST node
* @returns {Object|null} Category object or null
*/
function extractCategoryFromCall(node) {
if (node.arguments.length < 2) {
return null;
}
const category = {
name: null,
label: null,
description: null,
};
// First argument: category name (string literal)
const nameArg = node.arguments[0];
if (nameArg.type === 'Literal' && typeof nameArg.value === 'string') {
category.name = nameArg.value;
} else {
return null; // Name must be a string literal
}
// Second argument: config object
const configArg = node.arguments[1];
if (configArg.type === 'ObjectExpression') {
configArg.properties.forEach((prop) => {
if (prop.type === 'Property' && prop.key.type === 'Identifier') {
const key = prop.key.name;
if (
(key === 'label' || key === 'description') &&
prop.value.type === 'Literal' &&
typeof prop.value.value === 'string'
) {
category[key] = prop.value.value;
}
}
});
}
return category;
}
/**
* Validate category data.
*
* @param {Object} category - Category data
* @param {Array} errors - Error messages array
* @param {Array} warnings - Warning messages array
*/
function validateCategory(category, errors, warnings) {
const { name, label, description } = category;
// Validate category name format (kebab-case)
if (!/^[a-z0-9-]+$/.test(name)) {
errors.push(
`Invalid category name format: '${name}'. Must be kebab-case (lowercase, hyphens allowed)`
);
}
// Check required fields
if (!label) {
errors.push(`Missing required field 'label' in category '${name}'`);
} else {
// Validate label quality
if (label.trim().length < 2) {
warnings.push(
`Label for category '${name}' is too short. Provide a meaningful label.`
);
}
}
if (!description) {
errors.push(
`Missing required field 'description' in category '${name}'`
);
} else {
// Validate description quality
const descLength = description.trim().length;
if (descLength < 15) {
warnings.push(
`Description for category '${name}' is too short (${descLength} chars). Provide detailed information about what abilities belong in this category.`
);
}
if (/TODO/i.test(description)) {
warnings.push(
`Description for category '${name}' contains TODO placeholder. Replace with actual description.`
);
}
}
}
/**
* Simple AST walker.
*
* @param {Object} node - AST node
* @param {Function} callback - Function to call for each node
*/
function walk(node, callback) {
if (!node || typeof node !== 'object') {
return;
}
callback(node);
for (const key in node) {
if (
key === 'loc' ||
key === 'range' ||
key === 'start' ||
key === 'end'
) {
continue; // Skip position metadata
}
const value = node[key];
if (Array.isArray(value)) {
value.forEach((child) => walk(child, callback));
} else if (value && typeof value === 'object') {
walk(value, callback);
}
}
}
/**
* Output validation results.
*
* @param {string} filePath - File being validated
* @param {Array} errors - Error messages
* @param {Array} warnings - Warning messages
*/
function outputResults(filePath, errors, warnings) {
console.log('='.repeat(70));
console.log(`Validation Results: ${filePath}`);
console.log('='.repeat(70));
console.log('');
if (errors.length > 0) {
console.log(`ERRORS (${errors.length}):`);
errors.forEach((error) => console.log(`${error}`));
console.log('');
}
if (warnings.length > 0) {
console.log(`WARNINGS (${warnings.length}):`);
warnings.forEach((warning) => console.log(`${warning}`));
console.log('');
}
if (errors.length === 0 && warnings.length === 0) {
console.log('✓ All validations passed! No issues found.');
console.log('');
} else if (errors.length === 0) {
console.log('✓ Validation passed with warnings.');
console.log('');
} else {
console.log('✗ Validation failed. Please fix the errors above.');
console.log('');
}
}

View File

@@ -0,0 +1,325 @@
#!/usr/bin/env php
<?php
/**
* WordPress Ability Category Validation Script (PHP)
*
* Validates PHP category registration code using PHP's built-in tokenizer.
* For JavaScript validation, use validate-category.js instead.
*
* Usage:
* php validate-category.php path/to/category-file.php
*
* Arguments:
* file_path Path to the PHP file containing category registration code
*
* Exit Codes:
* 0 - Validation passed
* 1 - Validation failed
* 2 - File not found or invalid usage
*/
// Check for file argument
if ($argc < 2) {
fwrite(STDERR, "Usage: php validate-category.php path/to/category-file.php\n");
exit(2);
}
$file_path = $argv[1];
// Check if file exists
if (!file_exists($file_path)) {
fwrite(STDERR, "Error: File not found: {$file_path}\n");
exit(2);
}
// Check if file is PHP
$file_extension = pathinfo($file_path, PATHINFO_EXTENSION);
if ($file_extension !== 'php') {
fwrite(STDERR, "Error: This validator only supports PHP files. For JavaScript validation, use validate-category.js\n");
exit(2);
}
// Read file contents
$content = file_get_contents($file_path);
if ($content === false) {
fwrite(STDERR, "Error: Unable to read file: {$file_path}\n");
exit(2);
}
// Initialize validation results
$errors = [];
$warnings = [];
// Parse and validate PHP categories
$categories = parse_php_categories($content);
if (empty($categories)) {
$errors[] = "No wp_register_ability_category() calls found in file";
} else {
foreach ($categories as $category) {
echo "Validating category: {$category['name']}\n";
validate_category($category, $errors, $warnings);
echo "\n";
}
}
// Output final results
output_results($file_path, $errors, $warnings);
// Exit with appropriate code
exit(empty($errors) ? 0 : 1);
/**
* Parse PHP file to extract category registrations using token_get_all().
*
* @param string $content PHP file content
* @return array Array of category data
*/
function parse_php_categories($content) {
$tokens = token_get_all($content);
$categories = [];
$i = 0;
$count = count($tokens);
while ($i < $count) {
$token = $tokens[$i];
// Look for function call: wp_register_ability_category
if (is_array($token) && $token[0] === T_STRING && $token[1] === 'wp_register_ability_category') {
// Found the function call, now extract arguments
$category = extract_category_from_tokens($tokens, $i);
if ($category) {
$categories[] = $category;
}
}
$i++;
}
return $categories;
}
/**
* Extract category data from tokens starting at function call position.
*
* @param array $tokens All tokens
* @param int $start_pos Position of function name token
* @return array|null Category data or null if parsing fails
*/
function extract_category_from_tokens($tokens, $start_pos) {
$i = $start_pos + 1;
$count = count($tokens);
$category = ['name' => null, 'label' => null, 'description' => null];
// Skip whitespace to opening parenthesis
while ($i < $count && is_array($tokens[$i]) && $tokens[$i][0] === T_WHITESPACE) {
$i++;
}
// Should be at opening parenthesis
if ($i >= $count || $tokens[$i] !== '(') {
return null;
}
$i++;
// Skip whitespace to category name
while ($i < $count && is_array($tokens[$i]) && $tokens[$i][0] === T_WHITESPACE) {
$i++;
}
// Extract category name (first argument - string)
if ($i < $count && is_array($tokens[$i]) && ($tokens[$i][0] === T_CONSTANT_ENCAPSED_STRING)) {
$category['name'] = trim($tokens[$i][1], '\'"');
$i++;
} else {
return null; // No category name found
}
// Now look for the array with label and description
// Skip to 'array' keyword or '['
while ($i < $count) {
$token = $tokens[$i];
if (is_array($token) && $token[0] === T_ARRAY) {
// Found 'array(' syntax
$i++;
// Skip to opening parenthesis
while ($i < $count && $tokens[$i] !== '(') {
$i++;
}
if ($i < $count) {
$i++; // Move past '('
$extracted = extract_array_contents($tokens, $i);
$category = array_merge($category, $extracted);
}
break;
} elseif ($token === '[') {
// Found '[' short array syntax
$i++;
$extracted = extract_array_contents($tokens, $i);
$category = array_merge($category, $extracted);
break;
}
$i++;
}
return $category;
}
/**
* Extract label and description from array tokens.
*
* @param array $tokens All tokens
* @param int $start_pos Position after array opening
* @return array Array with label and description keys
*/
function extract_array_contents($tokens, $start_pos) {
$result = ['label' => null, 'description' => null];
$i = $start_pos;
$count = count($tokens);
$depth = 1; // Track nested arrays/parentheses
while ($i < $count && $depth > 0) {
$token = $tokens[$i];
// Track depth for nested structures
if ($token === '(' || $token === '[') {
$depth++;
} elseif ($token === ')' || $token === ']') {
$depth--;
if ($depth === 0) break;
}
// Look for 'label' or 'description' keys
if (is_array($token) && $token[0] === T_CONSTANT_ENCAPSED_STRING) {
$key = trim($token[1], '\'"');
if ($key === 'label' || $key === 'description') {
// Skip to the value (past '=>' and whitespace)
$i++;
// Skip whitespace
while ($i < $count && is_array($tokens[$i]) && $tokens[$i][0] === T_WHITESPACE) {
$i++;
}
// Skip '=>' (T_DOUBLE_ARROW)
if ($i < $count && is_array($tokens[$i]) && $tokens[$i][0] === T_DOUBLE_ARROW) {
$i++;
}
// Skip more whitespace
while ($i < $count && is_array($tokens[$i]) && $tokens[$i][0] === T_WHITESPACE) {
$i++;
}
// Check for __() translation function
if ($i < $count && is_array($tokens[$i]) && $tokens[$i][0] === T_STRING && $tokens[$i][1] === '__') {
// Skip to opening parenthesis of __()
while ($i < $count && $tokens[$i] !== '(') {
$i++;
}
if ($i < $count) {
$depth++; // Track the opening paren
$i++; // Move past '('
}
// Skip whitespace
while ($i < $count && is_array($tokens[$i]) && $tokens[$i][0] === T_WHITESPACE) {
$i++;
}
// Extract string value
if ($i < $count && is_array($tokens[$i]) && $tokens[$i][0] === T_CONSTANT_ENCAPSED_STRING) {
$result[$key] = trim($tokens[$i][1], '\'"');
}
} elseif ($i < $count && is_array($tokens[$i]) && $tokens[$i][0] === T_CONSTANT_ENCAPSED_STRING) {
// Direct string value
$result[$key] = trim($tokens[$i][1], '\'"');
}
// Continue to next iteration without incrementing again
continue;
}
}
$i++;
}
return $result;
}
/**
* Validate category data.
*
* @param array $category Category data
* @param array &$errors Error messages array
* @param array &$warnings Warning messages array
*/
function validate_category($category, &$errors, &$warnings) {
$name = $category['name'];
// Validate category name format (kebab-case)
if (!preg_match('/^[a-z0-9-]+$/', $name)) {
$errors[] = "Invalid category name format: '{$name}'. Must be kebab-case (lowercase, hyphens allowed)";
}
// Check required fields
if (empty($category['label'])) {
$errors[] = "Missing required field 'label' in category '{$name}'";
} else {
// Validate label quality
if (strlen(trim($category['label'])) < 2) {
$warnings[] = "Label for category '{$name}' is too short. Provide a meaningful label.";
}
}
if (empty($category['description'])) {
$errors[] = "Missing required field 'description' in category '{$name}'";
} else {
// Validate description quality
$desc_length = strlen(trim($category['description']));
if ($desc_length < 15) {
$warnings[] = "Description for category '{$name}' is too short ({$desc_length} chars). Provide detailed information about what abilities belong in this category.";
}
if (stripos($category['description'], 'TODO') !== false) {
$warnings[] = "Description for category '{$name}' contains TODO placeholder. Replace with actual description.";
}
}
}
/**
* Output validation results.
*
* @param string $file_path File being validated
* @param array $errors Error messages
* @param array $warnings Warning messages
*/
function output_results($file_path, $errors, $warnings) {
echo str_repeat('=', 70) . "\n";
echo "Validation Results: {$file_path}\n";
echo str_repeat('=', 70) . "\n\n";
if (!empty($errors)) {
echo "ERRORS (" . count($errors) . "):\n";
foreach ($errors as $error) {
echo "{$error}\n";
}
echo "\n";
}
if (!empty($warnings)) {
echo "WARNINGS (" . count($warnings) . "):\n";
foreach ($warnings as $warning) {
echo "{$warning}\n";
}
echo "\n";
}
if (empty($errors) && empty($warnings)) {
echo "✓ All validations passed! No issues found.\n\n";
} elseif (empty($errors)) {
echo "✓ Validation passed with warnings.\n\n";
} else {
echo "✗ Validation failed. Please fix the errors above.\n\n";
}
}