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/ai-command.php b/ai-command.php index fab0242..e8ac370 100644 --- a/ai-command.php +++ b/ai-command.php @@ -22,4 +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 722acaf..5f46185 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,8 @@ "wp-cli/wp-cli": "^2.13" }, "require-dev": { + "wp-cli/entity-command": "^2", + "wp-cli/extension-command": "^2", "wp-cli/wp-cli-tests": "^5" }, "config": { @@ -27,14 +29,12 @@ "bundled": false, "commands": [ "ai", - "ai credentials", - "ai credentials get", - "ai credentials set", - "ai credentials delete", - "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..4a0c515 --- /dev/null +++ b/features/connectors.feature @@ -0,0 +1,208 @@ +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 with descriptions + When I run `wp connectors list --format=json` + 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":"Anthropic" + """ + And STDOUT should contain: + """ + "description":"Text generation with Claude." + """ + And STDOUT should contain: + """ + "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 contain: + """ + "name":"OpenAI" + """ + And STDOUT should contain: + """ + "status":"not installed" + """ + + @require-wp-7.0 + 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 + """ + And STDOUT should contain: + """ + Anthropic + """ + And STDOUT should contain: + """ + Google + """ + + @require-wp-7.0 + 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 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":""} + """ + + # 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: + """ + ai-provider-for-openai + """ + When I run `wp option update connectors_ai_openai_api_key sk-test123456789` + Then STDOUT should contain: + """ + Success + """ + + When I run `wp connectors get openai --format=json` + Then STDOUT should contain: + """ + "status": "connected" + """ + And STDOUT should contain: + """ + "api_key": "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u20226789" + """ + + @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 JSON containing: + """ + {"name":"OpenAI","auth_method":"api_key","type":"ai_provider","plugin_slug":"ai-provider-for-openai"} + """ + + @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 active status + Given these installed and active plugins: + """ + ai-provider-for-openai + """ + + When I run `wp connectors list --format=json` + Then STDOUT should contain: + """ + "name":"OpenAI" + """ + And STDOUT should contain: + """ + "status":"active" + """ + + When I run `wp connectors get openai --format=json` + Then STDOUT should be JSON containing: + """ + {"name":"OpenAI","status":"active"} + """ + + @require-wp-7.0 + Scenario: Filter by active status returns only active connectors + Given these installed and active plugins: + """ + ai-provider-for-openai + """ + + When I run `wp connectors list --status=active --format=json` + Then STDOUT should contain: + """ + "name":"OpenAI" + """ + And STDOUT should not contain: + """ + "name":"Anthropic" + """ + + # 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/aslamdoctor/ai-provider-for-grok/archive/refs/heads/master.zip --activate` + And I run `wp connectors list --format=json` + Then STDOUT should contain: + """ + "status":"active" + """ + And STDOUT should contain: + """ + Grok + """ + 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/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. diff --git a/src/Connectors_Command.php b/src/Connectors_Command.php new file mode 100644 index 0000000..823461e --- /dev/null +++ b/src/Connectors_Command.php @@ -0,0 +1,310 @@ +] + * : 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 + * + * @subcommand list + * @when after_wp_load + * + * @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 ) { + 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 ) { + 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; + } + ) + ); + } + + $format = $assoc_args['format'] ?? 'table'; + $fields = isset( $assoc_args['fields'] ) + ? explode( ',', $assoc_args['fields'] ) + : $this->default_fields; + + 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 with GPT and Dall-E | + * | status | connected | + * | credentials_url | https://platform.openai.com/api-keys | + * | api_key | ••••••••••••6789 | + * +-----------------+-----------------------------------------------+ + * + * @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 ] ) || ! is_array( $connectors[ $connector_id ] ) ) { + WP_CLI::error( sprintf( 'Connector "%s" not found.', $connector_id ) ); + } + + $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' === $method && '' !== $setting ) { + // The option_* filter registered by WP core masks the value automatically. + $raw = get_option( $setting, '' ); + $api_key = is_string( $raw ) ? $raw : ''; + } + + $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 ), + '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, + ); + } + + /** + * 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'; + } + + /** + * 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. + * + * @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; + } +} 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; - } -}