diff --git a/demo/wp-feature-api-agent/includes/class-wp-feature-register.php b/demo/wp-feature-api-agent/includes/class-wp-feature-register.php index 2780e60..22e3220 100644 --- a/demo/wp-feature-api-agent/includes/class-wp-feature-register.php +++ b/demo/wp-feature-api-agent/includes/class-wp-feature-register.php @@ -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' ), + ) + ); + } } /** diff --git a/demo/wp-feature-api-agent/wp-feature-api-agent.php b/demo/wp-feature-api-agent/wp-feature-api-agent.php index 41f15a1..6f2a701 100644 --- a/demo/wp-feature-api-agent/wp-feature-api-agent.php +++ b/demo/wp-feature-api-agent/wp-feature-api-agent.php @@ -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. diff --git a/includes/class-wp-feature-api-init.php b/includes/class-wp-feature-api-init.php index e1f13e2..fae9554 100644 --- a/includes/class-wp-feature-api-init.php +++ b/includes/class-wp-feature-api-init.php @@ -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 ); } /** @@ -85,6 +90,7 @@ public static function register_rest_routes() { $controller->register_routes(); } + /** * Loads the WP Feature API Demo plugin. * diff --git a/package-lock.json b/package-lock.json index 85894f4..40fe904 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wp-feature-api", - "version": "0.1.2", + "version": "0.1.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wp-feature-api", - "version": "0.1.2", + "version": "0.1.9", "license": "GPL-2.0-or-later", "workspaces": [ "packages/*", @@ -32745,7 +32745,7 @@ }, "packages/client": { "name": "@automattic/wp-feature-api", - "version": "0.1.2", + "version": "0.1.8", "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/api-fetch": "^6.55.0", diff --git a/package.json b/package.json index e8d42a9..52f4b1c 100644 --- a/package.json +++ b/package.json @@ -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, diff --git a/packages/client/src/api.ts b/packages/client/src/api.ts index e347271..284a362 100644 --- a/packages/client/src/api.ts +++ b/packages/client/src/api.ts @@ -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. @@ -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 diff --git a/packages/client/src/config.ts b/packages/client/src/config.ts new file mode 100644 index 0000000..7215a9c --- /dev/null +++ b/packages/client/src/config.ts @@ -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/' ) ) + ); +} diff --git a/packages/client/src/store/resolvers.ts b/packages/client/src/store/resolvers.ts index 643e62e..78772d7 100644 --- a/packages/client/src/store/resolvers.ts +++ b/packages/client/src/store/resolvers.ts @@ -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++; } } diff --git a/wp-feature-api.php b/wp-feature-api.php index 2d5c94a..b008e4a 100644 --- a/wp-feature-api.php +++ b/wp-feature-api.php @@ -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 @@ -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__ ); @@ -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, );