Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e6ffbfc
Initial plan
Copilot Dec 15, 2025
d4cacd0
Implement wp ai credentials command with subcommands
Copilot Dec 15, 2025
1871646
Fix API key masking expectations in tests
Copilot Dec 15, 2025
0be3439
Merge branch 'main' into copilot/scaffold-initial-implementation
swissspidy Dec 15, 2025
1500014
Update credentials option name and implement AI generate/check commands
Copilot Dec 15, 2025
b57579c
Address code review feedback: improve security and type hints
Copilot Dec 15, 2025
f561230
Enhance security: add comprehensive path and data validation
Copilot Dec 15, 2025
2373949
Refine validation: use PHP native base64 strict mode and add size con…
Copilot Dec 15, 2025
e9034cb
Final security hardening: improve path resolution and JSON encoding
Copilot Dec 15, 2025
7fa8fee
Add cross-platform security and improve constant clarity
Copilot Dec 15, 2025
128a5a9
Add missing Prompt_Builder options to generate command
Copilot Dec 15, 2025
2f608ac
Use spread operator instead of call_user_func_array for modern PHP
Copilot Dec 15, 2025
80ba4fc
Code cleanup: remove trailing whitespace and optimize loop
Copilot Dec 15, 2025
0b5e1d2
Add wp ai status command to check supported capabilities
Copilot Dec 15, 2025
4e2c834
Refactor: extract AI Client availability check to DRY helper method
Copilot Dec 16, 2025
abc333b
Code quality improvements: extract constants and improve path checks
Copilot Dec 16, 2025
7a816eb
Minor refinements: improve Windows path detection and clarify comment
Copilot Dec 16, 2025
a0145c0
Improve documentation: clarify capability check constant comment
Copilot Dec 16, 2025
bf3c7b8
Lint fixes
swissspidy Dec 16, 2025
afab24a
Some manual fixes
swissspidy Dec 17, 2025
d0f5595
Round of fixes
swissspidy Dec 17, 2025
46c6e94
Some fixes
swissspidy Dec 18, 2025
376fa4e
Fix credentials command
swissspidy Dec 18, 2025
c3338ce
Some fixes
swissspidy Dec 18, 2025
2fff296
Add --top-p and --top-k parameters to generate command
Copilot Dec 18, 2025
43bf2ca
update commands list
swissspidy Dec 18, 2025
23d9bbe
PHPStan fixes
swissspidy Dec 19, 2025
f5bb17a
Simplification
swissspidy Dec 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ai-command.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@
}

WP_CLI::add_command( 'ai', AI_Command::class );
WP_CLI::add_command( 'ai credentials', Credentials_Command::class );
10 changes: 9 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,15 @@
},
"bundled": false,
"commands": [
"ai"
"ai",
"ai credentials",
"ai credentials get",
"ai credentials set",
"ai credentials delete",
"ai credentials list",
"ai check",
"ai generate",
"ai status"
]
},
"autoload": {
Expand Down
105 changes: 105 additions & 0 deletions features/credentials.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
Feature: Manage AI provider credentials

Background:
Given a WP install

Scenario: List credentials when none exist
When I run `wp ai credentials list`
Then STDOUT should contain:
"""
No credentials found.
"""

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":"sk-*********6789"}]
"""

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":"sk-**********-123"
"""

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

Scenario: Error when getting non-existent credentials
When I try `wp ai credentials get nonexistent`
Then STDERR should contain:
"""
Error: Credentials for provider "nonexistent" not found.
"""
And the return code should be 1

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

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 | sk-*****i123 |
| anthropic | sk-*******-456 |

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":"new****-456"
"""
179 changes: 179 additions & 0 deletions features/generate.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
Feature: Generate AI content

Background:
Given a WP install
And a wp-content/mu-plugins/mock-provider.php file:
"""
<?php

use WordPress\AiClient\AiClient;
use WordPress\AiClient\Messages\DTO\MessagePart;
use WordPress\AiClient\Messages\DTO\ModelMessage;
use WordPress\AiClient\Messages\Enums\ModalityEnum;
use WordPress\AiClient\Providers\Contracts\ModelMetadataDirectoryInterface;
use WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface;
use WordPress\AiClient\Providers\Contracts\ProviderInterface;
use WordPress\AiClient\Providers\DTO\ProviderMetadata;
use WordPress\AiClient\Providers\Enums\ProviderTypeEnum;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\TextGeneration\Contracts\TextGenerationModelInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
use WordPress\AiClient\Providers\Models\DTO\SupportedOption;
use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum;
use WordPress\AiClient\Providers\Models\Enums\OptionEnum;
use WordPress\AiClient\Results\DTO\Candidate;
use WordPress\AiClient\Results\DTO\GenerativeAiResult;
use WordPress\AiClient\Results\DTO\TokenUsage;
use WordPress\AiClient\Results\Enums\FinishReasonEnum;
use WordPress\AI_Client\API_Credentials\API_Credentials_Manager;

class WP_CLI_Mock_Model implements ModelInterface, TextGenerationModelInterface {
private $id;
private $config;

public function __construct( $id ) {
$this->id = $id;
$this->config = new ModelConfig();
}

public function metadata(): ModelMetadata {
return new ModelMetadata(
$this->id,
'WP-CLI Mock Model',
// Supported capabilities.
[
CapabilityEnum::textGeneration(),
CapabilityEnum::imageGeneration(),
],
// Supported options.
[
new SupportedOption(
OptionEnum::inputModalities(),
[
[ModalityEnum::text()]
]
),
new SupportedOption(
OptionEnum::outputModalities(),
[
[ModalityEnum::text()],
[ModalityEnum::text(), ModalityEnum::image()],
]
),
]
);
}

public function providerMetadata(): ProviderMetadata {
return WP_CLI_Mock_Provider::metadata();
}

public function setConfig( ModelConfig $config ): void {
$this->config = $config;
}

public function getConfig(): ModelConfig {
return $this->config;
}

public function generateTextResult(array $prompt): GenerativeAiResult {
// throw new RuntimeException('No candidates were generated');

$modelMessage = new ModelMessage([
new MessagePart('Generated content')
]);
$candidate = new Candidate(
$modelMessage,
FinishReasonEnum::stop(),
42
);
$tokenUsage = new TokenUsage(10, 42, 52);
return new GenerativeAiResult(
'result_123',
[ $candidate ],
$tokenUsage,
$this->providerMetadata(),
$this->metadata(),
[ 'provider' => 'wp-cli-mock-provider' ]
);
}

public function streamGenerateTextResult(array $prompt): Generator {
yield from [];
}
}

class WP_CLI_Mock_Provider implements ProviderInterface {
public static function metadata(): ProviderMetadata {
return new ProviderMetadata( 'wp-cli-mock-provider', 'WP-CLI Mock Provider', ProviderTypeEnum::cloud() );
}

public static function model( string $modelId, ?ModelConfig $modelConfig = null ): ModelInterface {
return new WP_CLI_Mock_Model( $modelId );
}

public static function availability(): ProviderAvailabilityInterface {
return new class() implements ProviderAvailabilityInterface {
public function isConfigured(): bool {
return true;
}
};
}

public static function modelMetadataDirectory(): ModelMetadataDirectoryInterface {
return new class() implements ModelMetadataDirectoryInterface {
public function listModelMetadata(): array {
return [
( new WP_CLI_Mock_Model( 'wp-cli-mock-model' ) )->metadata()
];
}

public function hasModelMetadata( string $modelId ): bool {
return true;
}

public function getModelMetadata( string $modelId ): ModelMetadata {
return self::model()->metadata();
}
};
}
}

WP_CLI::add_hook(
'ai_client_init',
static function () {
AiClient::defaultRegistry()->registerProvider( WP_CLI_Mock_Provider::class );

( new API_Credentials_Manager() )->initialize();
}
);
"""

Scenario: Generate command validates model format
When I try `wp ai generate text "Test prompt" --model=invalidformat`
Then the return code should be 1

Scenario: Generate command validates max-tokens
When I try `wp ai generate text "Test prompt" --max-tokens=-5`
Then the return code should be 1
And STDERR should contain:
"""
Max tokens must be a positive integer
"""

Scenario: Generate command validates top-p range
When I try `wp ai generate text "Test prompt" --top-p=1.5`
Then the return code should be 1
And STDERR should contain:
"""
Top-p must be between 0.0 and 1.0
"""

Scenario: Generate command validates top-k positive
When I try `wp ai generate text "Test prompt" --top-k=-10`
Then the return code should be 1
And STDERR should contain:
"""
Top-k must be a positive integer
"""
5 changes: 0 additions & 5 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,3 @@ parameters:
scanFiles:
- vendor/php-stubs/wordpress-stubs/wordpress-stubs.php
treatPhpDocTypesAsCertain: false
ignoreErrors:
# WP-CLI command method signatures use untyped arrays
-
message: '#no value type specified in iterable type array#'
reportUnmatched: false
Loading