Skip to content
This repository was archived by the owner on Nov 21, 2025. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 41 additions & 11 deletions demo/wp-feature-api-agent/includes/class-wp-feature-register.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,47 @@ class WP_Feature_Register {
* @return void
*/
public function register_features() {
/** Global Features */
wp_register_feature(
array(
'id' => 'demo/site-info',
'name' => __( 'Site Information', 'wp-feature-api-agent' ),
'description' => __( 'Get basic information about the WordPress site. This includes the name, description, URL, version, language, timezone, date format, time format, active plugins, and active theme.', 'wp-feature-api-agent' ),
'type' => WP_Feature::TYPE_RESOURCE,
'categories' => array( 'demo', 'site', 'information' ),
'callback' => array( $this, 'site_info_callback' ),
)
);
// Check if we should use abilities API and if it's available
$use_abilities = defined( 'WP_FEATURE_API_ABILITIES_BACKEND' ) &&
WP_FEATURE_API_ABILITIES_BACKEND &&
function_exists( 'wp_register_ability' );

if ( $use_abilities ) {
wp_register_ability(
'demo/site-info',
array(
'label' => __( 'Site Information', 'wp-feature-api-agent' ),
'description' => __( 'Get basic information about the WordPress site. This includes the name, description, URL, version, language, timezone, date format, time format, active plugins, and active theme.', 'wp-feature-api-agent' ),
'execute_callback' => array( $this, 'site_info_callback' ),
'permission_callback' => function() {
return current_user_can( 'read' );
},
'input_schema' => array(
'type' => 'object',
'properties' => (object) array(),
),
'output_schema' => array(
'type' => 'object',
),
'meta' => array(
'type' => 'resource',
'categories' => array( 'demo', 'site', 'information' ),
'location' => 'server',
),
)
);
} else {
wp_register_feature(
array(
'id' => 'demo/site-info',
'name' => __( 'Site Information', 'wp-feature-api-agent' ),
'description' => __( 'Get basic information about the WordPress site. This includes the name, description, URL, version, language, timezone, date format, time format, active plugins, and active theme.', 'wp-feature-api-agent' ),
'type' => WP_Feature::TYPE_RESOURCE,
'categories' => array( 'demo', 'site', 'information' ),
'callback' => array( $this, 'site_info_callback' ),
)
);
}
}

/**
Expand Down
3 changes: 3 additions & 0 deletions demo/wp-feature-api-agent/wp-feature-api-agent.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@

// Register additional demo features.
$feature_register_instance = new A8C\WpFeatureApiAgent\WP_Feature_Register();

// Register on both hooks to support both APIs
add_action( 'wp_feature_api_init', array( $feature_register_instance, 'register_features' ) );
add_action( 'abilities_api_init', array( $feature_register_instance, 'register_features' ) );

/**
* Enqueues scripts and styles for the admin area.
Expand Down
6 changes: 6 additions & 0 deletions includes/class-wp-feature-api-init.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ public static function enqueue_admin_scripts() {
$assets = require WP_FEATURE_API_PLUGIN_DIR . 'build/index.asset.php';
wp_enqueue_script( 'wp-features', WP_FEATURE_API_PLUGIN_URL . 'build/index.js', $assets['dependencies'], $assets['version'], true );

$config = array();
if ( defined( 'WP_FEATURE_API_ABILITIES_BACKEND' ) && WP_FEATURE_API_ABILITIES_BACKEND ) {
$config['backend'] = 'abilities';
}
wp_localize_script( 'wp-features', 'wpFeatureAPIConfig', $config );
}

/**
Expand Down Expand Up @@ -85,6 +90,7 @@ public static function register_rest_routes() {
$controller->register_routes();
}


/**
* Loads the WP Feature API Demo plugin.
*
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "wp-feature-api",
"version": "0.1.8",
"version": "0.1.9",
"description": "A system for exposing WordPress functionality in a standardized, discoverable way for both server and client-side use",
"main": "src/index.js",
"private": true,
Expand Down
8 changes: 6 additions & 2 deletions packages/client/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import apiFetch from '@wordpress/api-fetch';
import { store } from './store';
import type { Feature } from './types';
import { removeNullValues } from './utils';
import { getRunEndpoint, shouldUseAbilitiesBackend } from './config';

/**
* Registers a feature with the feature registry.
Expand Down Expand Up @@ -81,9 +82,12 @@ export async function executeFeature(
return await callback( args );
}

// Server-side features
const method = feature.type === 'tool' ? 'POST' : 'GET';
let requestPath = `/wp/v2/features/${ featureId }/run`;
const useAbilities = shouldUseAbilitiesBackend( feature );
let requestPath = useAbilities
? getRunEndpoint( featureId )
: `/wp/v2/features/${ featureId }/run`;

const options: { method: string; data?: any } = { method };

// The LLM may pass in a bunch of new values for things, that can cause validation errors for certain
Expand Down
125 changes: 125 additions & 0 deletions packages/client/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* Configuration for Feature API client
*
* @since 0.1.0
*/

/**
* Internal dependencies
*/
import { ENTITY_NAME } from './store/constants';

export interface FeatureAPIConfig {
/**
* Backend to use for server features ('features' or 'abilities')
*/
backend: 'features' | 'abilities';

/**
* Entity name for fetching features/abilities
*/
entityName: string;
}

declare global {
interface Window {
wpFeatureAPIConfig?: {
backend?: string;
};
wp?: any;
}
}

const backend =
window.wpFeatureAPIConfig?.backend === 'abilities'
? 'abilities'
: 'features';

// Set configuration once at module load
const currentConfig: FeatureAPIConfig = {
backend,
entityName: backend === 'abilities' ? 'abilities' : ENTITY_NAME,
};

// Register abilities entity with core-data if using abilities backend
// wp-data should be available since it's a dependency of wp-features
if ( backend === 'abilities' ) {
registerAbilitiesEntity();
}

function registerAbilitiesEntity() {
const { dispatch, select } = ( window as any ).wp.data;
const coreStore = ( window as any ).wp.coreData?.store;

if ( coreStore && dispatch && select ) {
const entities = select( coreStore ).getEntitiesConfig( 'root' ) || [];
const abilitiesEntityExists = entities.some(
( entity: any ) => entity.name === 'abilities'
);
if ( ! abilitiesEntityExists ) {
dispatch( coreStore ).addEntities( [
{
name: 'abilities',
kind: 'root',
baseURL: '/wp/v2/abilities',
baseURLParams: { context: 'edit' },
plural: 'abilities',
label: 'Abilities',
key: 'name', // Abilities use 'name' as their identifier, not 'id'
},
] );
}
}
}

/**
* Get current configuration
*
* @return Current configuration
*/
export function getConfig(): FeatureAPIConfig {
return { ...currentConfig };
}

/**
* Get the entity name for API calls
*
* @return Entity name (features or abilities)
*/
export function getEntityName(): string {
return currentConfig.entityName || ENTITY_NAME;
}

/**
* Get the run endpoint path for a feature/ability
*
* @param id Feature or ability ID
* @return REST endpoint path
*/
export function getRunEndpoint( id: string ): string {
const entityName = getEntityName();
const basePath = '/wp/v2';

// For abilities, strip the 'ability/' prefix if present
const cleanId =
currentConfig.backend === 'abilities' && id.startsWith( 'ability/' )
? id.replace( 'ability/', '' )
: id;

return `${ basePath }/${ entityName }/${ cleanId }/run`;
}

/**
* Check if a feature should use the abilities backend
*
* @param feature Feature object
* @return Whether to use abilities backend
*/
export function shouldUseAbilitiesBackend( feature: any ): boolean {
// If abilities backend is enabled and the feature is server-side or has ability prefix
return (
currentConfig.backend === 'abilities' &&
( feature?.location === 'server' ||
feature?.id?.startsWith( 'ability/' ) )
);
}
29 changes: 25 additions & 4 deletions packages/client/src/store/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,48 @@ import { store as coreStore } from '@wordpress/core-data';
import { ENTITY_KIND, ENTITY_NAME } from './constants';
import { receiveFeatures, receiveFeature } from './actions';
import { store } from './index';
import { getConfig, getEntityName } from '../config';

export function getRegisteredFeatures() {
return async ( { dispatch, registry } ) => {
const config = getConfig();
const entityName = getEntityName();

// Start with page 1
let page = 1;
let allFeatures = [];
let hasMore = true;

// Keep fetching pages until we have all features
// Keep fetching pages until we have all features/abilities
while ( hasMore ) {
const features = await registry
.resolveSelect( coreStore )
.getEntityRecords( ENTITY_KIND, ENTITY_NAME, {
.getEntityRecords( ENTITY_KIND, entityName, {
page,
per_page: 100,
} );

if ( ! features || features.length === 0 ) {
hasMore = false;
} else {
allFeatures = [ ...allFeatures, ...features ];
// If fetching from abilities, convert them to feature format
const processedFeatures =
config.backend === 'abilities'
? features.map( ( ability ) => ( {
...ability,
// Use the ability's name field as the id, with 'ability/' prefix
id: ability.name?.startsWith( 'ability/' )
? ability.name
: `ability/${ ability.name }`,
name: ability.label || ability.name,
description: ability.description || '',
type: ability.meta?.type || 'tool',
categories: ability.meta?.categories || [],
location: ability.meta?.location || 'server',
} ) )
: features;

allFeatures = [ ...allFeatures, ...processedFeatures ];
page++;
}
}
Expand Down
8 changes: 4 additions & 4 deletions wp-feature-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Plugin Name: WordPress Feature API
* Plugin URI: https://github.com/Automattic/wp-feature-api
* Description: A system for exposing server and client-side functionality in WordPress for use in LLMs and agentic systems.
* Version: 0.1.8
* Version: 0.1.9
* Author: Automattic AI
* Author URI: https://automattic.ai/
* Text Domain: wp-feature-api
Expand All @@ -18,7 +18,7 @@
exit;
}

$wp_feature_api_version = '0.1.8';
$wp_feature_api_version = '0.1.9';
$wp_feature_api_plugin_dir = plugin_dir_path( __FILE__ );
$wp_feature_api_plugin_url = plugin_dir_url( __FILE__ );

Expand Down Expand Up @@ -53,8 +53,8 @@ function wp_feature_api_register_version( $version, $file ) {
// Generate a unique key using version + path hash to prevent overwriting
// installations of the same version in different directories. This is so we can
// provide a preference for the plugin/developer version over a vendor version.
$unique_key = $version . '|' . md5($file);
$wp_feature_api_versions[$unique_key] = array(
$unique_key = $version . '|' . md5( $file );
$wp_feature_api_versions[ $unique_key ] = array(
'version' => $version,
'file' => $file,
);
Expand Down