From 677198b5789fda36b88b9f2b2f58ac1037e2b60d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:53:02 +0000 Subject: [PATCH 01/15] Initial plan From d8b8ae4edc746c25377b76081a96fd2e9735d37b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:58:36 +0000 Subject: [PATCH 02/15] Add wp connectors list and wp connectors get commands Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- ai-command.php | 1 + composer.json | 5 +- features/connectors.feature | 126 ++++++++++++++++++++ src/Connectors_Command.php | 231 ++++++++++++++++++++++++++++++++++++ 4 files changed, 362 insertions(+), 1 deletion(-) create mode 100644 features/connectors.feature create mode 100644 src/Connectors_Command.php diff --git a/ai-command.php b/ai-command.php index fab0242..1b14e34 100644 --- a/ai-command.php +++ b/ai-command.php @@ -23,3 +23,4 @@ WP_CLI::add_command( 'ai', AI_Command::class, [ 'before_invoke' => $wpcli_ai_before_invoke ] ); WP_CLI::add_command( 'ai credentials', Credentials_Command::class, [ 'before_invoke' => $wpcli_ai_before_invoke ] ); +WP_CLI::add_command( 'connectors', Connectors_Command::class, [ 'before_invoke' => $wpcli_ai_before_invoke ] ); diff --git a/composer.json b/composer.json index 722acaf..e70e344 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,10 @@ "ai credentials list", "ai check", "ai generate", - "ai status" + "ai status", + "connectors", + "connectors get", + "connectors list" ] }, "autoload": { diff --git a/features/connectors.feature b/features/connectors.feature new file mode 100644 index 0000000..197b335 --- /dev/null +++ b/features/connectors.feature @@ -0,0 +1,126 @@ +Feature: List and get AI connectors + + Background: + Given a WP install + + @less-than-wp-7.0 + Scenario: Command not available on WP < 7.0 + When I try `wp connectors list` + Then STDERR should contain: + """ + Requires WordPress 7.0 or greater. + """ + And the return code should be 1 + + @require-wp-7.0 + Scenario: List connectors returns built-in providers + When I run `wp connectors list --format=json` + Then STDOUT should be a JSON array containing: + """ + [{"name":"OpenAI","type":"ai_provider","auth_method":"api_key","plugin_slug":"ai-provider-for-openai"}] + """ + And STDOUT should be a JSON array containing: + """ + [{"name":"Anthropic","type":"ai_provider","auth_method":"api_key","plugin_slug":"ai-provider-for-anthropic"}] + """ + And STDOUT should be a JSON array containing: + """ + [{"name":"Google","type":"ai_provider","auth_method":"api_key","plugin_slug":"ai-provider-for-google"}] + """ + + @require-wp-7.0 + Scenario: List connectors shows plugin install status as No when plugins are absent + When I run `wp connectors list --format=json` + Then STDOUT should be a JSON array containing: + """ + [{"name":"OpenAI","installed":"No","active":"No"}] + """ + + @require-wp-7.0 + Scenario: List connectors in table format + When I run `wp connectors list` + Then STDOUT should contain: + """ + OpenAI + """ + And STDOUT should contain: + """ + Anthropic + """ + And STDOUT should contain: + """ + Google + """ + + @require-wp-7.0 + Scenario: Get a specific connector + When I run `wp connectors get openai --format=json` + Then STDOUT should be a JSON array containing: + """ + [{"name":"OpenAI","type":"ai_provider","auth_method":"api_key","plugin_slug":"ai-provider-for-openai","installed":"No","active":"No","api_key":""}] + """ + + @require-wp-7.0 + Scenario: Get a connector shows masked API key when set + When I run `wp ai credentials set openai --api-key=sk-test123456789` + Then STDOUT should contain: + """ + Success: Credentials for provider "openai" have been saved. + """ + + When I run `wp connectors get openai --format=json` + Then STDOUT should be a JSON array containing: + """ + [{"name":"OpenAI","api_key":"••••••••••••6789"}] + """ + + @require-wp-7.0 + Scenario: Get connector with fields filter + When I run `wp connectors get openai --fields=name,auth_method --format=json` + Then STDOUT should be a JSON array containing: + """ + [{"name":"OpenAI","auth_method":"api_key"}] + """ + + @require-wp-7.0 + Scenario: Error on non-existent connector + When I try `wp connectors get nonexistent` + Then STDERR should contain: + """ + Error: Connector "nonexistent" not found. + """ + And the return code should be 1 + + @require-wp-7.0 + Scenario: Install OpenAI provider plugin and verify installed status + Given these installed and active plugins: + | plugin | + | ai-provider-for-openai | + + When I run `wp connectors list --format=json` + Then STDOUT should be a JSON array containing: + """ + [{"name":"OpenAI","installed":"Yes","active":"Yes"}] + """ + + When I run `wp connectors get openai --format=json` + Then STDOUT should be a JSON array containing: + """ + [{"name":"OpenAI","installed":"Yes","active":"Yes"}] + """ + + @require-wp-7.0 + Scenario: Community plugin shows up in connectors list + Given these installed and active plugins: + | plugin | + | ai-provider-for-openrouter | + + When I run `wp connectors list --format=json` + Then STDOUT should be a JSON array containing: + """ + [{"type":"ai_provider"}] + """ + And STDOUT should contain: + """ + ai-provider-for-openrouter + """ diff --git a/src/Connectors_Command.php b/src/Connectors_Command.php new file mode 100644 index 0000000..4611bff --- /dev/null +++ b/src/Connectors_Command.php @@ -0,0 +1,231 @@ +] + * : Comma-separated list of fields to include in the output. + * --- + * default: name,type,auth_method,plugin_slug,installed,active + * --- + * + * [--format=] + * : Render output in a particular format. + * --- + * default: table + * options: + * - table + * - csv + * - json + * - yaml + * --- + * + * ## EXAMPLES + * + * # List all connectors + * $ wp connectors list + * +----------+-------------+-------------+------------------------+-----------+--------+ + * | name | type | auth_method | plugin_slug | installed | active | + * +----------+-------------+-------------+------------------------+-----------+--------+ + * | Anthropic | ai_provider | api_key | ai-provider-for-anthropic | No | No | + * +----------+-------------+-------------+------------------------+-----------+--------+ + * + * @subcommand list + * @when after_wp_load + * + * @param string[] $args Positional arguments. Unused. + * @param array{fields: string, format: string} $assoc_args Associative arguments. + * @return void + */ + public function list_( $args, $assoc_args ) { + if ( ! function_exists( '_wp_connectors_get_connector_settings' ) ) { + WP_CLI::error( 'Requires WordPress 7.0 or greater.' ); + } + + $connectors = _wp_connectors_get_connector_settings(); + + $items = array(); + foreach ( $connectors as $connector_id => $connector ) { + $plugin_slug = isset( $connector['plugin']['slug'] ) ? (string) $connector['plugin']['slug'] : ''; + $installed = ''; + $active = ''; + + if ( $plugin_slug ) { + $installed = $this->is_plugin_installed( $plugin_slug ) ? 'Yes' : 'No'; + $active = $this->is_plugin_active( $plugin_slug ) ? 'Yes' : 'No'; + } + + $items[] = array( + 'name' => $connector['name'], + 'description' => $connector['description'], + 'type' => $connector['type'], + 'auth_method' => $connector['authentication']['method'], + 'credentials_url' => $connector['authentication']['credentials_url'] ?? '', + 'plugin_slug' => $plugin_slug, + 'installed' => $installed, + 'active' => $active, + ); + } + + $format = $assoc_args['format'] ?? 'table'; + $fields = isset( $assoc_args['fields'] ) + ? explode( ',', $assoc_args['fields'] ) + : array( 'name', 'type', 'auth_method', 'plugin_slug', 'installed', 'active' ); + + WP_CLI\Utils\format_items( $format, $items, $fields ); + } + + /** + * Gets details about a specific AI connector. + * + * ## OPTIONS + * + * + * : The connector ID (e.g., openai, anthropic, google). + * + * [--fields=] + * : Comma-separated list of fields to include in the output. + * + * [--format=] + * : Render output in a particular format. + * --- + * default: table + * options: + * - table + * - csv + * - json + * - yaml + * --- + * + * ## EXAMPLES + * + * # Get details for the OpenAI connector + * $ wp connectors get openai + * +------------------+-------------------------------+ + * | Field | Value | + * +------------------+-------------------------------+ + * | name | OpenAI | + * | description | Text and image generation ... | + * +------------------+-------------------------------+ + * + * @when after_wp_load + * + * @param array{0: string} $args Positional arguments. + * @param array{fields: string, format: string} $assoc_args Associative arguments. + * @return void + */ + public function get( $args, $assoc_args ) { + if ( ! function_exists( '_wp_connectors_get_connector_settings' ) ) { + WP_CLI::error( 'Requires WordPress 7.0 or greater.' ); + } + + list( $connector_id ) = $args; + + $connectors = _wp_connectors_get_connector_settings(); + + if ( ! isset( $connectors[ $connector_id ] ) ) { + WP_CLI::error( sprintf( 'Connector "%s" not found.', $connector_id ) ); + } + + $connector = $connectors[ $connector_id ]; + $auth = $connector['authentication']; + $plugin_slug = isset( $connector['plugin']['slug'] ) ? (string) $connector['plugin']['slug'] : ''; + + $api_key = ''; + if ( 'api_key' === $auth['method'] && ! empty( $auth['setting_name'] ) ) { + // The option_* filter registered by WP core masks the value automatically. + $raw = get_option( $auth['setting_name'], '' ); + $api_key = is_string( $raw ) ? $raw : ''; + } + + $installed = ''; + $active = ''; + if ( $plugin_slug ) { + $installed = $this->is_plugin_installed( $plugin_slug ) ? 'Yes' : 'No'; + $active = $this->is_plugin_active( $plugin_slug ) ? 'Yes' : 'No'; + } + + $item = array( + 'name' => $connector['name'], + 'description' => $connector['description'], + 'type' => $connector['type'], + 'auth_method' => $auth['method'], + 'credentials_url' => $auth['credentials_url'] ?? '', + 'plugin_slug' => $plugin_slug, + 'installed' => $installed, + 'active' => $active, + 'api_key' => $api_key, + ); + + $format = $assoc_args['format'] ?? 'table'; + $fields = isset( $assoc_args['fields'] ) + ? explode( ',', $assoc_args['fields'] ) + : array_keys( $item ); + + WP_CLI\Utils\format_items( $format, array( $item ), $fields ); + } + + /** + * Checks whether a plugin is installed, given its slug. + * + * @param string $slug The WordPress.org plugin slug. + * @return bool + */ + private function is_plugin_installed( string $slug ): bool { + if ( ! function_exists( 'get_plugins' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + foreach ( array_keys( get_plugins() ) as $plugin_file ) { + if ( strpos( $plugin_file, $slug . '/' ) === 0 || $plugin_file === $slug . '.php' ) { + return true; + } + } + + return false; + } + + /** + * Checks whether a plugin is active, given its slug. + * + * @param string $slug The WordPress.org plugin slug. + * @return bool + */ + private function is_plugin_active( string $slug ): bool { + if ( ! function_exists( 'is_plugin_active' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + $active_plugins = (array) get_option( 'active_plugins', array() ); + foreach ( $active_plugins as $plugin_file ) { + if ( ! is_string( $plugin_file ) ) { + continue; + } + if ( strpos( $plugin_file, $slug . '/' ) === 0 || $plugin_file === $slug . '.php' ) { + return true; + } + } + + return false; + } +} From 39612205c089e3c2e8e414d6d38227662fe9258c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 13:47:50 +0000 Subject: [PATCH 03/15] Refine connectors output: description column, status column, hide plugin_slug/type/auth_method by default Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- features/connectors.feature | 71 ++++++++++++++++++------- src/Connectors_Command.php | 101 ++++++++++++++++++++++++------------ 2 files changed, 119 insertions(+), 53 deletions(-) diff --git a/features/connectors.feature b/features/connectors.feature index 197b335..0bf6de8 100644 --- a/features/connectors.feature +++ b/features/connectors.feature @@ -17,29 +17,49 @@ Feature: List and get AI connectors When I run `wp connectors list --format=json` Then STDOUT should be a JSON array containing: """ - [{"name":"OpenAI","type":"ai_provider","auth_method":"api_key","plugin_slug":"ai-provider-for-openai"}] + [{"name":"OpenAI","description":"Text and image generation with GPT and Dall-E."}] """ And STDOUT should be a JSON array containing: """ - [{"name":"Anthropic","type":"ai_provider","auth_method":"api_key","plugin_slug":"ai-provider-for-anthropic"}] + [{"name":"Anthropic","description":"Text generation with Claude."}] """ And STDOUT should be a JSON array containing: """ - [{"name":"Google","type":"ai_provider","auth_method":"api_key","plugin_slug":"ai-provider-for-google"}] + [{"name":"Google","description":"Text and image generation with Gemini and Imagen."}] """ @require-wp-7.0 - Scenario: List connectors shows plugin install status as No when plugins are absent + Scenario: List connectors shows not-installed status when plugins are absent When I run `wp connectors list --format=json` Then STDOUT should be a JSON array containing: """ - [{"name":"OpenAI","installed":"No","active":"No"}] + [{"name":"OpenAI","status":"not installed"}] + """ + And STDOUT should be a JSON array containing: + """ + [{"name":"Anthropic","status":"not installed"}] + """ + And STDOUT should be a JSON array containing: + """ + [{"name":"Google","status":"not installed"}] """ @require-wp-7.0 - Scenario: List connectors in table format + Scenario: List connectors in table format shows name, description and status columns When I run `wp connectors list` Then STDOUT should contain: + """ + name + """ + And STDOUT should contain: + """ + description + """ + And STDOUT should contain: + """ + status + """ + And STDOUT should contain: """ OpenAI """ @@ -57,11 +77,11 @@ Feature: List and get AI connectors When I run `wp connectors get openai --format=json` Then STDOUT should be a JSON array containing: """ - [{"name":"OpenAI","type":"ai_provider","auth_method":"api_key","plugin_slug":"ai-provider-for-openai","installed":"No","active":"No","api_key":""}] + [{"name":"OpenAI","description":"Text and image generation with GPT and Dall-E.","status":"not installed","credentials_url":"https://platform.openai.com/api-keys","api_key":""}] """ @require-wp-7.0 - Scenario: Get a connector shows masked API key when set + Scenario: Get a connector shows masked API key and connected status when set When I run `wp ai credentials set openai --api-key=sk-test123456789` Then STDOUT should contain: """ @@ -71,15 +91,15 @@ Feature: List and get AI connectors When I run `wp connectors get openai --format=json` Then STDOUT should be a JSON array containing: """ - [{"name":"OpenAI","api_key":"••••••••••••6789"}] + [{"name":"OpenAI","status":"connected","api_key":"••••••••••••6789"}] """ @require-wp-7.0 - Scenario: Get connector with fields filter - When I run `wp connectors get openai --fields=name,auth_method --format=json` + Scenario: Get connector with fields filter including hidden fields + When I run `wp connectors get openai --fields=name,auth_method,type,plugin_slug --format=json` Then STDOUT should be a JSON array containing: """ - [{"name":"OpenAI","auth_method":"api_key"}] + [{"name":"OpenAI","auth_method":"api_key","type":"ai_provider","plugin_slug":"ai-provider-for-openai"}] """ @require-wp-7.0 @@ -92,7 +112,7 @@ Feature: List and get AI connectors And the return code should be 1 @require-wp-7.0 - Scenario: Install OpenAI provider plugin and verify installed status + Scenario: Install OpenAI provider plugin and verify active status Given these installed and active plugins: | plugin | | ai-provider-for-openai | @@ -100,27 +120,40 @@ Feature: List and get AI connectors When I run `wp connectors list --format=json` Then STDOUT should be a JSON array containing: """ - [{"name":"OpenAI","installed":"Yes","active":"Yes"}] + [{"name":"OpenAI","status":"active"}] """ When I run `wp connectors get openai --format=json` Then STDOUT should be a JSON array containing: """ - [{"name":"OpenAI","installed":"Yes","active":"Yes"}] + [{"name":"OpenAI","status":"active"}] + """ + + @require-wp-7.0 + Scenario: Connected status when plugin is active and API key is configured + Given these installed and active plugins: + | plugin | + | ai-provider-for-openai | + + When I run `wp ai credentials set openai --api-key=sk-test123456789` + And I run `wp connectors list --format=json` + Then STDOUT should be a JSON array containing: + """ + [{"name":"OpenAI","status":"connected"}] """ @require-wp-7.0 Scenario: Community plugin shows up in connectors list Given these installed and active plugins: - | plugin | - | ai-provider-for-openrouter | + | plugin | + | ai-provider-for-azure-openai | When I run `wp connectors list --format=json` Then STDOUT should be a JSON array containing: """ - [{"type":"ai_provider"}] + [{"status":"active"}] """ And STDOUT should contain: """ - ai-provider-for-openrouter + Azure """ diff --git a/src/Connectors_Command.php b/src/Connectors_Command.php index 4611bff..6a9a98d 100644 --- a/src/Connectors_Command.php +++ b/src/Connectors_Command.php @@ -26,7 +26,7 @@ class Connectors_Command extends WP_CLI_Command { * [--fields=] * : Comma-separated list of fields to include in the output. * --- - * default: name,type,auth_method,plugin_slug,installed,active + * default: name,description,status * --- * * [--format=] @@ -44,11 +44,13 @@ class Connectors_Command extends WP_CLI_Command { * * # List all connectors * $ wp connectors list - * +----------+-------------+-------------+------------------------+-----------+--------+ - * | name | type | auth_method | plugin_slug | installed | active | - * +----------+-------------+-------------+------------------------+-----------+--------+ - * | Anthropic | ai_provider | api_key | ai-provider-for-anthropic | No | No | - * +----------+-------------+-------------+------------------------+-----------+--------+ + * +-----------+-----------------------------------------------+---------------+ + * | name | description | status | + * +-----------+-----------------------------------------------+---------------+ + * | Anthropic | Text generation with Claude. | not installed | + * | Google | Text and image generation with Gemini... | not installed | + * | OpenAI | Text and image generation with GPT and Dall-E | connected | + * +-----------+-----------------------------------------------+---------------+ * * @subcommand list * @when after_wp_load @@ -67,30 +69,22 @@ public function list_( $args, $assoc_args ) { $items = array(); foreach ( $connectors as $connector_id => $connector ) { $plugin_slug = isset( $connector['plugin']['slug'] ) ? (string) $connector['plugin']['slug'] : ''; - $installed = ''; - $active = ''; - - if ( $plugin_slug ) { - $installed = $this->is_plugin_installed( $plugin_slug ) ? 'Yes' : 'No'; - $active = $this->is_plugin_active( $plugin_slug ) ? 'Yes' : 'No'; - } $items[] = array( 'name' => $connector['name'], 'description' => $connector['description'], + 'status' => $this->get_connector_status( $connector_id, $connector ), 'type' => $connector['type'], 'auth_method' => $connector['authentication']['method'], 'credentials_url' => $connector['authentication']['credentials_url'] ?? '', 'plugin_slug' => $plugin_slug, - 'installed' => $installed, - 'active' => $active, ); } $format = $assoc_args['format'] ?? 'table'; $fields = isset( $assoc_args['fields'] ) ? explode( ',', $assoc_args['fields'] ) - : array( 'name', 'type', 'auth_method', 'plugin_slug', 'installed', 'active' ); + : array( 'name', 'description', 'status' ); WP_CLI\Utils\format_items( $format, $items, $fields ); } @@ -105,6 +99,9 @@ public function list_( $args, $assoc_args ) { * * [--fields=] * : Comma-separated list of fields to include in the output. + * --- + * default: name,description,status,credentials_url,api_key + * --- * * [--format=] * : Render output in a particular format. @@ -121,12 +118,15 @@ public function list_( $args, $assoc_args ) { * * # Get details for the OpenAI connector * $ wp connectors get openai - * +------------------+-------------------------------+ - * | Field | Value | - * +------------------+-------------------------------+ - * | name | OpenAI | - * | description | Text and image generation ... | - * +------------------+-------------------------------+ + * +-----------------+-----------------------------------------------+ + * | Field | Value | + * +-----------------+-----------------------------------------------+ + * | name | OpenAI | + * | description | Text and image generation with GPT and Dall-E | + * | status | connected | + * | credentials_url | https://platform.openai.com/api-keys | + * | api_key | ••••••••••••6789 | + * +-----------------+-----------------------------------------------+ * * @when after_wp_load * @@ -158,33 +158,66 @@ public function get( $args, $assoc_args ) { $api_key = is_string( $raw ) ? $raw : ''; } - $installed = ''; - $active = ''; - if ( $plugin_slug ) { - $installed = $this->is_plugin_installed( $plugin_slug ) ? 'Yes' : 'No'; - $active = $this->is_plugin_active( $plugin_slug ) ? 'Yes' : 'No'; - } - $item = array( 'name' => $connector['name'], 'description' => $connector['description'], + 'status' => $this->get_connector_status( $connector_id, $connector ), + 'credentials_url' => $auth['credentials_url'] ?? '', + 'api_key' => $api_key, 'type' => $connector['type'], 'auth_method' => $auth['method'], - 'credentials_url' => $auth['credentials_url'] ?? '', 'plugin_slug' => $plugin_slug, - 'installed' => $installed, - 'active' => $active, - 'api_key' => $api_key, ); + $default_fields = array( 'name', 'description', 'status', 'credentials_url', 'api_key' ); + $format = $assoc_args['format'] ?? 'table'; $fields = isset( $assoc_args['fields'] ) ? explode( ',', $assoc_args['fields'] ) - : array_keys( $item ); + : $default_fields; WP_CLI\Utils\format_items( $format, array( $item ), $fields ); } + /** + * Returns the status of a connector. + * + * Possible values: 'connected', 'active', 'installed', 'not installed'. + * + * @param string $connector_id The connector ID. + * @param mixed[] $connector Connector settings from _wp_connectors_get_connector_settings(). + * @return string + */ + private function get_connector_status( string $connector_id, array $connector ): string { + $auth = is_array( $connector['authentication'] ) ? $connector['authentication'] : array(); + $plugin = isset( $connector['plugin'] ) && is_array( $connector['plugin'] ) ? $connector['plugin'] : array(); + $plugin_slug = isset( $plugin['slug'] ) && is_string( $plugin['slug'] ) ? $plugin['slug'] : ''; + $method = isset( $auth['method'] ) && is_string( $auth['method'] ) ? $auth['method'] : ''; + $setting = isset( $auth['setting_name'] ) && is_string( $auth['setting_name'] ) ? $auth['setting_name'] : ''; + + if ( 'api_key' === $method && '' !== $setting ) { + // The option_* filter registered by WP core masks the value automatically. + $raw = get_option( $setting, '' ); + if ( is_string( $raw ) && '' !== $raw ) { + return 'connected'; + } + } + + if ( ! $plugin_slug ) { + return 'active'; + } + + if ( $this->is_plugin_active( $plugin_slug ) ) { + return 'active'; + } + + if ( $this->is_plugin_installed( $plugin_slug ) ) { + return 'installed'; + } + + return 'not installed'; + } + /** * Checks whether a plugin is installed, given its slug. * From 4968b0c4a40a6ac388d90bb36a314567e7b27bec Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 5 Mar 2026 16:48:45 +0100 Subject: [PATCH 04/15] Apply suggestions from code review --- features/connectors.feature | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/features/connectors.feature b/features/connectors.feature index 0bf6de8..2a0f670 100644 --- a/features/connectors.feature +++ b/features/connectors.feature @@ -132,8 +132,9 @@ Feature: List and get AI connectors @require-wp-7.0 Scenario: Connected status when plugin is active and API key is configured Given these installed and active plugins: - | plugin | - | ai-provider-for-openai | + """ + ai-provider-for-openai + """ When I run `wp ai credentials set openai --api-key=sk-test123456789` And I run `wp connectors list --format=json` @@ -145,8 +146,9 @@ Feature: List and get AI connectors @require-wp-7.0 Scenario: Community plugin shows up in connectors list Given these installed and active plugins: - | plugin | - | ai-provider-for-azure-openai | + """ + ai-provider-for-azure-openai + """ When I run `wp connectors list --format=json` Then STDOUT should be a JSON array containing: From eba233c43f27980f37ef92e3e3263c5a21b9903c Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 5 Mar 2026 18:18:54 +0100 Subject: [PATCH 05/15] Apply suggestion from @swissspidy --- features/connectors.feature | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/features/connectors.feature b/features/connectors.feature index 2a0f670..1018fd4 100644 --- a/features/connectors.feature +++ b/features/connectors.feature @@ -114,8 +114,9 @@ Feature: List and get AI connectors @require-wp-7.0 Scenario: Install OpenAI provider plugin and verify active status Given these installed and active plugins: - | plugin | - | ai-provider-for-openai | + """ + ai-provider-for-openai + """ When I run `wp connectors list --format=json` Then STDOUT should be a JSON array containing: From a0ff5513ab6a49d20016cb57f3a936187df5ccfe Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 5 Mar 2026 21:09:48 +0100 Subject: [PATCH 06/15] Add extension command for tests --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index e70e344..c93e52a 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,7 @@ "wp-cli/wp-cli": "^2.13" }, "require-dev": { + "wp-cli/extension-command": "^2", "wp-cli/wp-cli-tests": "^5" }, "config": { From 1734a8d658e2d7f586ecd796a35070a4dc738a21 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 5 Mar 2026 21:47:40 +0100 Subject: [PATCH 07/15] Fix test --- features/connectors.feature | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/features/connectors.feature b/features/connectors.feature index 1018fd4..bce689f 100644 --- a/features/connectors.feature +++ b/features/connectors.feature @@ -146,12 +146,8 @@ Feature: List and get AI connectors @require-wp-7.0 Scenario: Community plugin shows up in connectors list - Given these installed and active plugins: - """ - ai-provider-for-azure-openai - """ - - When I run `wp connectors list --format=json` + When I run `wp plugin install https://github.com/soderlind/ai-provider-for-azure-openai/archive/refs/heads/main.zip --activate` + And I run `wp connectors list --format=json` Then STDOUT should be a JSON array containing: """ [{"status":"active"}] From 2b568334c45c85c0db5a3e1968090b37903f8c00 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 5 Mar 2026 21:59:57 +0100 Subject: [PATCH 08/15] Some fixes --- src/Connectors_Command.php | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Connectors_Command.php b/src/Connectors_Command.php index 6a9a98d..d375816 100644 --- a/src/Connectors_Command.php +++ b/src/Connectors_Command.php @@ -17,6 +17,16 @@ * $ wp connectors get openai */ class Connectors_Command extends WP_CLI_Command { + /** + * Default fields for list output. + * + * @var string[] + */ + protected $default_fields = [ + 'name', + 'description', + 'status', + ]; /** * Lists all available AI connectors. @@ -25,9 +35,6 @@ class Connectors_Command extends WP_CLI_Command { * * [--fields=] * : Comma-separated list of fields to include in the output. - * --- - * default: name,description,status - * --- * * [--format=] * : Render output in a particular format. @@ -84,7 +91,7 @@ public function list_( $args, $assoc_args ) { $format = $assoc_args['format'] ?? 'table'; $fields = isset( $assoc_args['fields'] ) ? explode( ',', $assoc_args['fields'] ) - : array( 'name', 'description', 'status' ); + : $this->default_fields; WP_CLI\Utils\format_items( $format, $items, $fields ); } @@ -99,9 +106,6 @@ public function list_( $args, $assoc_args ) { * * [--fields=] * : Comma-separated list of fields to include in the output. - * --- - * default: name,description,status,credentials_url,api_key - * --- * * [--format=] * : Render output in a particular format. @@ -169,12 +173,10 @@ public function get( $args, $assoc_args ) { 'plugin_slug' => $plugin_slug, ); - $default_fields = array( 'name', 'description', 'status', 'credentials_url', 'api_key' ); - $format = $assoc_args['format'] ?? 'table'; $fields = isset( $assoc_args['fields'] ) ? explode( ',', $assoc_args['fields'] ) - : $default_fields; + : array_keys( $item ); WP_CLI\Utils\format_items( $format, array( $item ), $fields ); } From 76f56a44761441d37160f84db06ed6587c549954 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 21:14:13 +0000 Subject: [PATCH 09/15] Fix test fatal errors, add --status filter, pivot layout for get, remove credentials commands Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> --- ai-command.php | 1 - composer.json | 5 - features/connectors.feature | 118 ++++++++++----- features/credentials.feature | 124 ---------------- src/Connectors_Command.php | 108 ++++++++++---- src/Credentials_Command.php | 279 ----------------------------------- 6 files changed, 154 insertions(+), 481 deletions(-) delete mode 100644 features/credentials.feature delete mode 100644 src/Credentials_Command.php diff --git a/ai-command.php b/ai-command.php index 1b14e34..e8ac370 100644 --- a/ai-command.php +++ b/ai-command.php @@ -22,5 +22,4 @@ }; WP_CLI::add_command( 'ai', AI_Command::class, [ 'before_invoke' => $wpcli_ai_before_invoke ] ); -WP_CLI::add_command( 'ai credentials', Credentials_Command::class, [ 'before_invoke' => $wpcli_ai_before_invoke ] ); WP_CLI::add_command( 'connectors', Connectors_Command::class, [ 'before_invoke' => $wpcli_ai_before_invoke ] ); diff --git a/composer.json b/composer.json index c93e52a..19001fb 100644 --- a/composer.json +++ b/composer.json @@ -28,11 +28,6 @@ "bundled": false, "commands": [ "ai", - "ai credentials", - "ai credentials get", - "ai credentials set", - "ai credentials delete", - "ai credentials list", "ai check", "ai generate", "ai status", diff --git a/features/connectors.feature b/features/connectors.feature index bce689f..4e3466b 100644 --- a/features/connectors.feature +++ b/features/connectors.feature @@ -13,35 +13,39 @@ Feature: List and get AI connectors And the return code should be 1 @require-wp-7.0 - Scenario: List connectors returns built-in providers + Scenario: List connectors returns built-in providers with descriptions When I run `wp connectors list --format=json` - Then STDOUT should be a JSON array containing: + Then STDOUT should contain: + """ + "name":"OpenAI" + """ + And STDOUT should contain: + """ + "description":"Text and image generation with GPT and Dall-E." + """ + And STDOUT should contain: """ - [{"name":"OpenAI","description":"Text and image generation with GPT and Dall-E."}] + "name":"Anthropic" """ - And STDOUT should be a JSON array containing: + And STDOUT should contain: """ - [{"name":"Anthropic","description":"Text generation with Claude."}] + "description":"Text generation with Claude." """ - And STDOUT should be a JSON array containing: + And STDOUT should contain: """ - [{"name":"Google","description":"Text and image generation with Gemini and Imagen."}] + "name":"Google" """ @require-wp-7.0 Scenario: List connectors shows not-installed status when plugins are absent When I run `wp connectors list --format=json` - Then STDOUT should be a JSON array containing: - """ - [{"name":"OpenAI","status":"not installed"}] - """ - And STDOUT should be a JSON array containing: + Then STDOUT should contain: """ - [{"name":"Anthropic","status":"not installed"}] + "name":"OpenAI" """ - And STDOUT should be a JSON array containing: + And STDOUT should contain: """ - [{"name":"Google","status":"not installed"}] + "status":"not installed" """ @require-wp-7.0 @@ -73,33 +77,61 @@ Feature: List and get AI connectors """ @require-wp-7.0 - Scenario: Get a specific connector + Scenario: List connectors supports --status filter + When I run `wp connectors list --status="not installed" --format=json` + Then STDOUT should contain: + """ + "name":"OpenAI" + """ + And STDOUT should contain: + """ + "name":"Anthropic" + """ + + @require-wp-7.0 + Scenario: Get a specific connector shows key-value layout + When I run `wp connectors get openai` + Then STDOUT should contain: + """ + name + """ + And STDOUT should contain: + """ + OpenAI + """ + And STDOUT should contain: + """ + status + """ + + @require-wp-7.0 + Scenario: Get a specific connector in JSON format When I run `wp connectors get openai --format=json` - Then STDOUT should be a JSON array containing: + Then STDOUT should be JSON containing: """ - [{"name":"OpenAI","description":"Text and image generation with GPT and Dall-E.","status":"not installed","credentials_url":"https://platform.openai.com/api-keys","api_key":""}] + {"name":"OpenAI","description":"Text and image generation with GPT and Dall-E.","status":"not installed","credentials_url":"https://platform.openai.com/api-keys","api_key":""} """ @require-wp-7.0 Scenario: Get a connector shows masked API key and connected status when set - When I run `wp ai credentials set openai --api-key=sk-test123456789` + When I run `wp option update connectors_ai_openai_api_key sk-test123456789` Then STDOUT should contain: """ - Success: Credentials for provider "openai" have been saved. + Success """ When I run `wp connectors get openai --format=json` - Then STDOUT should be a JSON array containing: + Then STDOUT should be JSON containing: """ - [{"name":"OpenAI","status":"connected","api_key":"••••••••••••6789"}] + {"name":"OpenAI","status":"connected","api_key":"••••••••••••6789"} """ @require-wp-7.0 Scenario: Get connector with fields filter including hidden fields When I run `wp connectors get openai --fields=name,auth_method,type,plugin_slug --format=json` - Then STDOUT should be a JSON array containing: + Then STDOUT should be JSON containing: """ - [{"name":"OpenAI","auth_method":"api_key","type":"ai_provider","plugin_slug":"ai-provider-for-openai"}] + {"name":"OpenAI","auth_method":"api_key","type":"ai_provider","plugin_slug":"ai-provider-for-openai"} """ @require-wp-7.0 @@ -114,45 +146,51 @@ Feature: List and get AI connectors @require-wp-7.0 Scenario: Install OpenAI provider plugin and verify active status Given these installed and active plugins: - """ - ai-provider-for-openai - """ + | plugin | + | ai-provider-for-openai | When I run `wp connectors list --format=json` - Then STDOUT should be a JSON array containing: + Then STDOUT should contain: + """ + "name":"OpenAI" """ - [{"name":"OpenAI","status":"active"}] + And STDOUT should contain: + """ + "status":"active" """ When I run `wp connectors get openai --format=json` - Then STDOUT should be a JSON array containing: + Then STDOUT should be JSON containing: """ - [{"name":"OpenAI","status":"active"}] + {"name":"OpenAI","status":"active"} """ @require-wp-7.0 - Scenario: Connected status when plugin is active and API key is configured + Scenario: Filter by active status returns only active connectors Given these installed and active plugins: + | plugin | + | ai-provider-for-openai | + + When I run `wp connectors list --status=active --format=json` + Then STDOUT should contain: """ - ai-provider-for-openai + "name":"OpenAI" """ - - When I run `wp ai credentials set openai --api-key=sk-test123456789` - And I run `wp connectors list --format=json` - Then STDOUT should be a JSON array containing: + And STDOUT should not contain: """ - [{"name":"OpenAI","status":"connected"}] + "name":"Anthropic" """ @require-wp-7.0 Scenario: Community plugin shows up in connectors list When I run `wp plugin install https://github.com/soderlind/ai-provider-for-azure-openai/archive/refs/heads/main.zip --activate` And I run `wp connectors list --format=json` - Then STDOUT should be a JSON array containing: + Then STDOUT should contain: """ - [{"status":"active"}] + "status":"active" """ And STDOUT should contain: """ Azure """ + diff --git a/features/credentials.feature b/features/credentials.feature deleted file mode 100644 index 15febbc..0000000 --- a/features/credentials.feature +++ /dev/null @@ -1,124 +0,0 @@ -Feature: Manage AI provider credentials - - Background: - Given a WP install - - @less-than-wp-7.0 - Scenario: Command not available on WP < 7.0 - Given a WP install - - When I try `wp ai credentials list` - Then STDERR should contain: - """ - Requires WordPress 7.0 or greater. - """ - And the return code should be 1 - - @require-wp-7.0 - Scenario: List credentials when none exist - When I run `wp ai credentials list` - Then STDOUT should contain: - """ - No credentials found. - """ - - @require-wp-7.0 - Scenario: Set and list credentials - When I run `wp ai credentials set openai --api-key=sk-test123456789` - Then STDOUT should contain: - """ - Success: Credentials for provider "openai" have been saved. - """ - - When I run `wp ai credentials list --format=json` - Then STDOUT should be JSON containing: - """ - [{"provider":"openai","api_key":"••••••••••••6789"}] - """ - - @require-wp-7.0 - Scenario: Get specific provider credentials - When I run `wp ai credentials set anthropic --api-key=sk-ant-api-key-123` - Then STDOUT should contain: - """ - Success: Credentials for provider "anthropic" have been saved. - """ - - When I run `wp ai credentials get anthropic --format=json` - Then STDOUT should contain: - """ - "provider":"anthropic" - """ - And STDOUT should contain: - """ - "api_key":"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022-123" - """ - - @require-wp-7.0 - Scenario: Delete provider credentials - When I run `wp ai credentials set google --api-key=test-google-key` - Then STDOUT should contain: - """ - Success: Credentials for provider "google" have been saved. - """ - - When I run `wp ai credentials delete google` - Then STDOUT should contain: - """ - Success: Credentials for provider "google" have been deleted. - """ - - When I try `wp ai credentials get google` - Then STDERR should contain: - """ - Error: Credentials for provider "google" not found. - """ - And the return code should be 1 - - @require-wp-7.0 - Scenario: Error when getting non-existent credentials - When I try `wp ai credentials get nonexistent` - Then STDERR should contain: - """ - Error: Provider "nonexistent" is not a supported AI connector. - """ - And the return code should be 1 - - @require-wp-7.0 - Scenario: Error when setting credentials without api-key - When I try `wp ai credentials set openai` - Then STDERR should contain: - """ - missing --api-key parameter - """ - And the return code should be 1 - - @require-wp-7.0 - Scenario: List multiple credentials in table format - When I run `wp ai credentials set openai --api-key=sk-openai123` - And I run `wp ai credentials set anthropic --api-key=sk-ant-api-456` - And I run `wp ai credentials list` - Then STDOUT should be a table containing rows: - | provider | api_key | - | openai | ••••••••i123 | - | anthropic | ••••••••••-456 | - - @require-wp-7.0 - Scenario: Update existing credentials - When I run `wp ai credentials set openai --api-key=old-key-123` - Then STDOUT should contain: - """ - Success: Credentials for provider "openai" have been saved. - """ - - When I run `wp ai credentials set openai --api-key=new-key-456` - Then STDOUT should contain: - """ - Success: Credentials for provider "openai" have been saved. - """ - - When I run `wp ai credentials get openai --format=json` - Then STDOUT should contain: - """ - "api_key":"\u2022\u2022\u2022\u2022\u2022\u2022\u2022-456" - """ diff --git a/src/Connectors_Command.php b/src/Connectors_Command.php index d375816..823461e 100644 --- a/src/Connectors_Command.php +++ b/src/Connectors_Command.php @@ -33,6 +33,16 @@ class Connectors_Command extends WP_CLI_Command { * * ## OPTIONS * + * [--status=] + * : Filter connectors by status. + * --- + * options: + * - connected + * - active + * - installed + * - not installed + * --- + * * [--fields=] * : Comma-separated list of fields to include in the output. * @@ -59,11 +69,14 @@ class Connectors_Command extends WP_CLI_Command { * | OpenAI | Text and image generation with GPT and Dall-E | connected | * +-----------+-----------------------------------------------+---------------+ * + * # List only connected connectors + * $ wp connectors list --status=connected + * * @subcommand list * @when after_wp_load * - * @param string[] $args Positional arguments. Unused. - * @param array{fields: string, format: string} $assoc_args Associative arguments. + * @param string[] $args Positional arguments. Unused. + * @param array{status: string, fields: string, format: string} $assoc_args Associative arguments. * @return void */ public function list_( $args, $assoc_args ) { @@ -75,16 +88,22 @@ public function list_( $args, $assoc_args ) { $items = array(); foreach ( $connectors as $connector_id => $connector ) { - $plugin_slug = isset( $connector['plugin']['slug'] ) ? (string) $connector['plugin']['slug'] : ''; - - $items[] = array( - 'name' => $connector['name'], - 'description' => $connector['description'], - 'status' => $this->get_connector_status( $connector_id, $connector ), - 'type' => $connector['type'], - 'auth_method' => $connector['authentication']['method'], - 'credentials_url' => $connector['authentication']['credentials_url'] ?? '', - 'plugin_slug' => $plugin_slug, + if ( ! is_array( $connector ) ) { + continue; + } + + $items[] = $this->build_connector_item( $connector_id, $connector ); + } + + if ( isset( $assoc_args['status'] ) ) { + $status_filter = $assoc_args['status']; + $items = array_values( + array_filter( + $items, + static function ( array $item ) use ( $status_filter ) { + return $item['status'] === $status_filter; + } + ) ); } @@ -147,38 +166,53 @@ public function get( $args, $assoc_args ) { $connectors = _wp_connectors_get_connector_settings(); - if ( ! isset( $connectors[ $connector_id ] ) ) { + if ( ! isset( $connectors[ $connector_id ] ) || ! is_array( $connectors[ $connector_id ] ) ) { WP_CLI::error( sprintf( 'Connector "%s" not found.', $connector_id ) ); } - $connector = $connectors[ $connector_id ]; - $auth = $connector['authentication']; - $plugin_slug = isset( $connector['plugin']['slug'] ) ? (string) $connector['plugin']['slug'] : ''; + $connector = $connectors[ $connector_id ]; + $item = $this->build_connector_item( $connector_id, $connector ); + + // Retrieve and append the (possibly masked) API key. + $auth = is_array( $connector['authentication'] ) ? $connector['authentication'] : array(); + $method = isset( $auth['method'] ) && is_string( $auth['method'] ) ? $auth['method'] : ''; + $setting = isset( $auth['setting_name'] ) && is_string( $auth['setting_name'] ) ? $auth['setting_name'] : ''; $api_key = ''; - if ( 'api_key' === $auth['method'] && ! empty( $auth['setting_name'] ) ) { + if ( 'api_key' === $method && '' !== $setting ) { // The option_* filter registered by WP core masks the value automatically. - $raw = get_option( $auth['setting_name'], '' ); + $raw = get_option( $setting, '' ); $api_key = is_string( $raw ) ? $raw : ''; } - $item = array( - 'name' => $connector['name'], - 'description' => $connector['description'], + $item['api_key'] = $api_key; + + $default_fields = array( 'name', 'description', 'status', 'credentials_url', 'api_key' ); + $formatter = new \WP_CLI\Formatter( $assoc_args, $default_fields ); + $formatter->display_item( $item ); + } + + /** + * Builds a flat item array from a connector settings array. + * + * @param string $connector_id The connector ID. + * @param mixed[] $connector Connector settings from _wp_connectors_get_connector_settings(). + * @return array{name: string, description: string, status: string, type: string, auth_method: string, credentials_url: string, plugin_slug: string} + */ + private function build_connector_item( string $connector_id, array $connector ): array { + $auth = is_array( $connector['authentication'] ) ? $connector['authentication'] : array(); + $plugin = isset( $connector['plugin'] ) && is_array( $connector['plugin'] ) ? $connector['plugin'] : array(); + $plugin_slug = isset( $plugin['slug'] ) && is_string( $plugin['slug'] ) ? $plugin['slug'] : ''; + + return array( + 'name' => $this->scalar_to_string( $connector['name'] ?? '' ), + 'description' => $this->scalar_to_string( $connector['description'] ?? '' ), 'status' => $this->get_connector_status( $connector_id, $connector ), - 'credentials_url' => $auth['credentials_url'] ?? '', - 'api_key' => $api_key, - 'type' => $connector['type'], - 'auth_method' => $auth['method'], + 'type' => $this->scalar_to_string( $connector['type'] ?? '' ), + 'auth_method' => isset( $auth['method'] ) && is_string( $auth['method'] ) ? $auth['method'] : '', + 'credentials_url' => isset( $auth['credentials_url'] ) && is_string( $auth['credentials_url'] ) ? $auth['credentials_url'] : '', 'plugin_slug' => $plugin_slug, ); - - $format = $assoc_args['format'] ?? 'table'; - $fields = isset( $assoc_args['fields'] ) - ? explode( ',', $assoc_args['fields'] ) - : array_keys( $item ); - - WP_CLI\Utils\format_items( $format, array( $item ), $fields ); } /** @@ -220,6 +254,16 @@ private function get_connector_status( string $connector_id, array $connector ): return 'not installed'; } + /** + * Casts a scalar value to string, returning an empty string for non-scalars. + * + * @param mixed $value The value to cast. + * @return string + */ + private function scalar_to_string( $value ): string { + return is_scalar( $value ) ? (string) $value : ''; + } + /** * Checks whether a plugin is installed, given its slug. * diff --git a/src/Credentials_Command.php b/src/Credentials_Command.php deleted file mode 100644 index b444f9d..0000000 --- a/src/Credentials_Command.php +++ /dev/null @@ -1,279 +0,0 @@ -] - * : Render output in a particular format. - * --- - * default: table - * options: - * - table - * - csv - * - json - * - yaml - * --- - * - * ## EXAMPLES - * - * # List all credentials - * $ wp ai credentials list - * +----------+----------+ - * | provider | api_key | - * +----------+----------+ - * | openai | ••••••• | - * +----------+----------+ - * - * @subcommand list - * @when after_wp_load - * - * @param string[] $args Positional arguments. Unused. - * @param array{format: string} $assoc_args Associative arguments. - * @return void - */ - public function list_( $args, $assoc_args ) { - $credentials = $this->get_all_credentials(); - - if ( empty( $credentials ) ) { - WP_CLI::log( 'No credentials found.' ); - return; - } - - $items = array(); - foreach ( $credentials as $provider => $api_key ) { - $items[] = array( - 'provider' => $provider, - 'api_key' => $api_key, - ); - } - - $format = $assoc_args['format'] ?? 'table'; - WP_CLI\Utils\format_items( $format, $items, array( 'provider', 'api_key' ) ); - } - - /** - * Gets credentials for a specific AI provider. - * - * ## OPTIONS - * - * - * : The AI provider name (e.g., openai, anthropic, google). - * - * [--format=] - * : Render output in a particular format. - * --- - * default: json - * options: - * - json - * - yaml - * --- - * - * ## EXAMPLES - * - * # Get OpenAI credentials - * $ wp ai credentials get openai - * {"provider":"openai","api_key":"••••••••••••6789"} - * - * @when after_wp_load - * - * @param array{0: string} $args Positional arguments. - * @param array{format: string} $assoc_args Associative arguments. - * @return void - */ - public function get( $args, $assoc_args ) { - list( $provider ) = $args; - - $option_name = $this->get_connector_setting_name( $provider ); - $raw_key = get_option( $option_name, '' ); - $api_key = is_string( $raw_key ) ? $raw_key : ''; - - if ( '' === $api_key ) { - WP_CLI::error( sprintf( 'Credentials for provider "%s" not found.', $provider ) ); - } - - $data = array( - 'provider' => $provider, - 'api_key' => $api_key, - ); - - $format = $assoc_args['format'] ?? 'json'; - - if ( 'json' === $format ) { - WP_CLI::line( (string) json_encode( $data ) ); - } else { - // For yaml and other formats - foreach ( $data as $key => $value ) { - WP_CLI::line( "$key: $value" ); - } - } - } - - /** - * Sets or updates credentials for an AI provider. - * - * ## OPTIONS - * - * - * : The AI provider name (e.g., openai, anthropic, google). - * - * --api-key= - * : The API key for the provider. - * - * ## EXAMPLES - * - * # Set OpenAI credentials - * $ wp ai credentials set openai --api-key=sk-... - * Success: Credentials for provider "openai" have been saved. - * - * @when after_wp_load - * - * @param array{0: string} $args Positional arguments. - * @param array{'api-key': string} $assoc_args Associative array of associative arguments. - * @return void - */ - public function set( $args, $assoc_args ) { - list( $provider ) = $args; - - $api_key = $assoc_args['api-key']; - $option_name = $this->get_connector_setting_name( $provider ); - - // Remove any sanitize callback to bypass provider-side validation (e.g., live API checks). - remove_all_filters( "sanitize_option_{$option_name}" ); - update_option( $option_name, $api_key, false ); - - WP_CLI::success( sprintf( 'Credentials for provider "%s" have been saved.', $provider ) ); - } - - /** - * Deletes credentials for an AI provider. - * - * ## OPTIONS - * - * - * : The AI provider name (e.g., openai, anthropic, google). - * - * ## EXAMPLES - * - * # Delete OpenAI credentials - * $ wp ai credentials delete openai - * Success: Credentials for provider "openai" have been deleted. - * - * @when after_wp_load - * - * @param array{0: string} $args Positional arguments. - * @param array $assoc_args Associative arguments. Unused. - * @return void - */ - public function delete( $args, $assoc_args ) { - list( $provider ) = $args; - - $option_name = $this->get_connector_setting_name( $provider ); - $raw_key = get_option( $option_name, '' ); - $api_key = is_string( $raw_key ) ? $raw_key : ''; - - if ( '' === $api_key ) { - WP_CLI::error( sprintf( 'Credentials for provider "%s" not found.', $provider ) ); - } - - delete_option( $option_name ); - - WP_CLI::success( sprintf( 'Credentials for provider "%s" have been deleted.', $provider ) ); - } - - /** - * Gets the option name for a provider's API key from the connector registry. - * - * @param string $provider The connector/provider ID. - * @return string The option name. - */ - private function get_connector_setting_name( string $provider ): string { - if ( ! function_exists( '_wp_connectors_get_connector_settings' ) ) { - WP_CLI::error( 'Requires WordPress 7.0 or greater.' ); - } - - $settings = _wp_connectors_get_connector_settings(); - - if ( ! isset( $settings[ $provider ] ) ) { - WP_CLI::error( sprintf( 'Provider "%s" is not a supported AI connector.', $provider ) ); - } - - $setting_name = $this->get_api_key_setting_name( $settings[ $provider ]['authentication'] ?? [] ); - - if ( null === $setting_name ) { - WP_CLI::error( sprintf( 'Provider "%s" does not support API key authentication.', $provider ) ); - } - - return $setting_name; - } - - /** - * Returns the option/setting name if the given authentication config is an API key type, or null otherwise. - * - * @param mixed $auth Authentication config from the connector registry. - * @return string|null - */ - private function get_api_key_setting_name( $auth ): ?string { - if ( ! is_array( $auth ) || ! isset( $auth['method'] ) || 'api_key' !== $auth['method'] || empty( $auth['setting_name'] ) ) { - return null; - } - - return (string) $auth['setting_name']; - } - - /** - * Gets all credentials from the database. - * - * @return array - */ - private function get_all_credentials() { - if ( ! function_exists( '_wp_connectors_get_connector_settings' ) ) { - return array(); - } - - $credentials = array(); - - foreach ( _wp_connectors_get_connector_settings() as $connector_id => $connector_data ) { - $setting_name = $this->get_api_key_setting_name( $connector_data['authentication'] ?? [] ); - - if ( null === $setting_name ) { - continue; - } - - $raw = get_option( $setting_name, '' ); - $value = is_string( $raw ) ? $raw : ''; - if ( '' !== $value ) { - $credentials[ $connector_id ] = $value; - } - } - - ksort( $credentials ); - - return $credentials; - } -} From a751f9e0f5fd3086e7de8035196cd585c6731fea Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 5 Mar 2026 22:44:29 +0100 Subject: [PATCH 10/15] Fix tests --- features/connectors.feature | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/features/connectors.feature b/features/connectors.feature index 4e3466b..f28b726 100644 --- a/features/connectors.feature +++ b/features/connectors.feature @@ -146,8 +146,9 @@ Feature: List and get AI connectors @require-wp-7.0 Scenario: Install OpenAI provider plugin and verify active status Given these installed and active plugins: - | plugin | - | ai-provider-for-openai | + """ + ai-provider-for-openai + """ When I run `wp connectors list --format=json` Then STDOUT should contain: @@ -168,8 +169,9 @@ Feature: List and get AI connectors @require-wp-7.0 Scenario: Filter by active status returns only active connectors Given these installed and active plugins: - | plugin | - | ai-provider-for-openai | + """ + ai-provider-for-openai + """ When I run `wp connectors list --status=active --format=json` Then STDOUT should contain: From 8e5f9031fa54058bfb4dff95be9b28e67e774650 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 5 Mar 2026 22:45:02 +0100 Subject: [PATCH 11/15] Add entity command --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 19001fb..5f46185 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,7 @@ "wp-cli/wp-cli": "^2.13" }, "require-dev": { + "wp-cli/entity-command": "^2", "wp-cli/extension-command": "^2", "wp-cli/wp-cli-tests": "^5" }, From 3f9f5bf06a4e138d17467c388c71ff73c55f1cbf Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 11 Mar 2026 15:04:14 +0100 Subject: [PATCH 12/15] Fix readme --- README.md | 256 +++++++++++++++++++++------------------------ src/AI_Command.php | 2 +- 2 files changed, 118 insertions(+), 140 deletions(-) diff --git a/README.md b/README.md index 7bb43bc..2fba8c4 100644 --- a/README.md +++ b/README.md @@ -47,140 +47,6 @@ wp ai -### wp ai credentials - -Manages AI provider credentials. - -~~~ -wp ai credentials -~~~ - -**EXAMPLES** - - # List all stored AI provider credentials - $ wp ai credentials list - - # Get credentials for a specific provider - $ wp ai credentials get openai - - # Set credentials for a provider - $ wp ai credentials set openai --api-key=sk-... - - # Delete credentials for a provider - $ wp ai credentials delete openai - - - - - -### wp ai credentials get - -Gets credentials for a specific AI provider. - -~~~ -wp ai credentials get [--format=] -~~~ - -**OPTIONS** - - - The AI provider name (e.g., openai, anthropic, google). - - [--format=] - Render output in a particular format. - --- - default: json - options: - - json - - yaml - --- - -**EXAMPLES** - - # Get OpenAI credentials - $ wp ai credentials get openai - {"provider":"openai","api_key":"sk-*****"} - - - -### wp ai credentials set - -Sets or updates credentials for an AI provider. - -~~~ -wp ai credentials set --api-key= -~~~ - -**OPTIONS** - - - The AI provider name (e.g., openai, anthropic, google). - - --api-key= - The API key for the provider. - -**EXAMPLES** - - # Set OpenAI credentials - $ wp ai credentials set openai --api-key=sk-... - Success: Credentials for provider "openai" have been saved. - - - -### wp ai credentials delete - -Deletes credentials for an AI provider. - -~~~ -wp ai credentials delete -~~~ - -**OPTIONS** - - - The AI provider name (e.g., openai, anthropic, google). - -**EXAMPLES** - - # Delete OpenAI credentials - $ wp ai credentials delete openai - Success: Credentials for provider "openai" have been deleted. - - - -### wp ai credentials list - -Lists all stored AI provider credentials. - -~~~ -wp ai credentials list [--format=] -~~~ - -**OPTIONS** - - [--format=] - Render output in a particular format. - --- - default: table - options: - - table - - csv - - json - - yaml - --- - -**EXAMPLES** - - # List all credentials - $ wp ai credentials list - +----------+----------+ - | provider | api_key | - +----------+----------+ - | openai | sk-***** | - +----------+----------+ - - - ### wp ai check Checks if a prompt is supported for generation. @@ -217,7 +83,7 @@ wp ai check [--type=] Generates AI content. ~~~ -wp ai generate [--model=] [--provider=] [--temperature=] [--top-p=] [--top-k=] [--max-tokens=] [--system-instruction=] [--destination-file=] [--format=] +wp ai generate [--model=] [--provider=] [--temperature=] [--top-p=] [--top-k=] [--max-tokens=] [--system-instruction=] [--destination-file=] [--stdout] [--format=] ~~~ **OPTIONS** @@ -257,8 +123,8 @@ wp ai generate [--model=] [--provider=] [--tem [--destination-file=] For image generation, path to save the generated image. -[--stdout] -Output the whole image using standard output (incompatible with --destination-file=) + [--stdout] + Output the whole image using standard output (incompatible with --destination-file=) [--format=] Output format for text. @@ -326,9 +192,121 @@ are available. Displays a table showing supported capabilities. | Image Generation | No | +------------------+-----------+ + + +### wp connectors + +Lists and retrieves information about AI connectors. + +~~~ +wp connectors +~~~ + +**EXAMPLES** + + # List all available connectors + $ wp connectors list + + # Get details about a specific connector + $ wp connectors get openai + + + +### wp connectors get + +Gets details about a specific AI connector. + +~~~ +wp connectors get [--fields=] [--format=] +~~~ + +**OPTIONS** + + + The connector ID (e.g., openai, anthropic, google). + + [--fields=] + Comma-separated list of fields to include in the output. + + [--format=] + Render output in a particular format. + --- + default: table + options: + - table + - csv + - json + - yaml + --- + +**EXAMPLES** + + # Get details for the OpenAI connector + $ wp connectors get openai + +-----------------+-----------------------------------------------+ + | Field | Value | + +-----------------+-----------------------------------------------+ + | name | OpenAI | + | description | Text and image generation with GPT and Dall-E | + | status | connected | + | credentials_url | https://platform.openai.com/api-keys | + | api_key | ••••••••••••6789 | + +-----------------+-----------------------------------------------+ + + + +### wp connectors list + +Lists all available AI connectors. + +~~~ +wp connectors list [--status=] [--fields=] [--format=] +~~~ + +**OPTIONS** + + [--status=] + Filter connectors by status. + --- + options: + - connected + - active + - installed + - not installed + --- + + [--fields=] + Comma-separated list of fields to include in the output. + + [--format=] + Render output in a particular format. + --- + default: table + options: + - table + - csv + - json + - yaml + --- + +**EXAMPLES** + + # List all connectors + $ wp connectors list + +-----------+-----------------------------------------------+---------------+ + | name | description | status | + +-----------+-----------------------------------------------+---------------+ + | Anthropic | Text generation with Claude. | not installed | + | Google | Text and image generation with Gemini... | not installed | + | OpenAI | Text and image generation with GPT and Dall-E | connected | + +-----------+-----------------------------------------------+---------------+ + + # List only connected connectors + $ wp connectors list --status=connected + ## Installing -Installing this package requires WP-CLI v2.12 or greater. Update to the latest stable release with `wp cli update`. +Installing this package requires WP-CLI v2.13 or greater. Update to the latest stable release with `wp cli update`. Once you've done so, you can install the latest stable version of this package with: @@ -339,7 +317,7 @@ wp package install wp-cli/ai-command:@stable To install the latest development version of this package, use the following command instead: ```bash -wp package install wp-cli/ai-command:dev-main +wp package install wp-cli/ai-command:dev-master ``` ## Contributing diff --git a/src/AI_Command.php b/src/AI_Command.php index b28df24..81b9f12 100644 --- a/src/AI_Command.php +++ b/src/AI_Command.php @@ -81,7 +81,7 @@ class AI_Command extends WP_CLI_Command { * : For image generation, path to save the generated image. * * [--stdout] - * Output the whole image using standard output (incompatible with --destination-file=) + * : Output the whole image using standard output (incompatible with --destination-file=) * * [--format=] * : Output format for text. From 0d185baf78c5d57a099ffea2a969cfa6ba9cee79 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 11 Mar 2026 15:04:45 +0100 Subject: [PATCH 13/15] Update tests --- features/connectors.feature | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/features/connectors.feature b/features/connectors.feature index f28b726..98a2cfb 100644 --- a/features/connectors.feature +++ b/features/connectors.feature @@ -114,6 +114,10 @@ Feature: List and get AI connectors @require-wp-7.0 Scenario: Get a connector shows masked API key and connected status when set + Given these installed and active plugins: + """ + ai-provider-for-openai + """ When I run `wp option update connectors_ai_openai_api_key sk-test123456789` Then STDOUT should contain: """ @@ -121,9 +125,13 @@ Feature: List and get AI connectors """ When I run `wp connectors get openai --format=json` - Then STDOUT should be JSON containing: + Then STDOUT should contain: + """ + "status": "connected" + """ + And STDOUT should contain: """ - {"name":"OpenAI","status":"connected","api_key":"••••••••••••6789"} + "api_key": "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u20226789" """ @require-wp-7.0 From 29512556d8026b6546aac2da6976ba805870ad3c Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 11 Mar 2026 15:54:14 +0100 Subject: [PATCH 14/15] Fix tests --- features/connectors.feature | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/features/connectors.feature b/features/connectors.feature index 98a2cfb..f530e26 100644 --- a/features/connectors.feature +++ b/features/connectors.feature @@ -112,7 +112,8 @@ Feature: List and get AI connectors {"name":"OpenAI","description":"Text and image generation with GPT and Dall-E.","status":"not installed","credentials_url":"https://platform.openai.com/api-keys","api_key":""} """ - @require-wp-7.0 + # TODO: Depends on https://core.trac.wordpress.org/ticket/64819. + @require-wp-7.0 @broken Scenario: Get a connector shows masked API key and connected status when set Given these installed and active plugins: """ @@ -191,7 +192,8 @@ Feature: List and get AI connectors "name":"Anthropic" """ - @require-wp-7.0 + # Plugin requires PHP 7.4. + @require-wp-7.0 @require-php-7.4 Scenario: Community plugin shows up in connectors list When I run `wp plugin install https://github.com/soderlind/ai-provider-for-azure-openai/archive/refs/heads/main.zip --activate` And I run `wp connectors list --format=json` From 242e6fc8d40a459443e90c05b457d9dddedfeb38 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 11 Mar 2026 16:18:50 +0100 Subject: [PATCH 15/15] Use different plugin in test --- features/connectors.feature | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/connectors.feature b/features/connectors.feature index f530e26..4a0c515 100644 --- a/features/connectors.feature +++ b/features/connectors.feature @@ -195,7 +195,7 @@ Feature: List and get AI connectors # Plugin requires PHP 7.4. @require-wp-7.0 @require-php-7.4 Scenario: Community plugin shows up in connectors list - When I run `wp plugin install https://github.com/soderlind/ai-provider-for-azure-openai/archive/refs/heads/main.zip --activate` + When I run `wp plugin install https://github.com/aslamdoctor/ai-provider-for-grok/archive/refs/heads/master.zip --activate` And I run `wp connectors list --format=json` Then STDOUT should contain: """ @@ -203,6 +203,6 @@ Feature: List and get AI connectors """ And STDOUT should contain: """ - Azure + Grok """