PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.10
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.10
1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 All 34 releases
← All changes | includes/ai-copilot/abilities.php +104 -66 0.9.81.1.10 View file →
@@ -1,20 +1,20 @@
1 1 <?php
2 2 /**
3 - * Desktop Mode — AI Copilot abilities.
3 + * OpenStation — AI Copilot abilities.
4 4 *
5 5 * The Copilot's tools are WordPress Abilities API abilities: the agent loop
6 6 * offers the model every registered read-only ability (see
7 - * {@see desktop_mode_ai_search_ability_names()}) and runs a chosen one through
7 + * {@see openstation_ai_search_ability_names()}) and runs a chosen one through
8 8 * `wp_get_ability()->execute()` — permission checks and input validation
9 9 * happen inside `WP_Ability::execute()`.
10 10 *
11 11 * Every ability's `execute_callback` delegates to the existing query handlers
12 - * (via {@see desktop_mode_ai_search_dispatch_tool()} / the comment scorer) so
12 + * (via {@see openstation_ai_search_dispatch_tool()} / the comment scorer) so
13 13 * there is a single implementation of each tool. The ability is the source of
14 14 * truth for the model-facing description + input schema.
15 15 *
16 - * @package WPDesktopMode
16 + * @package OpenStation
17 17 */
18 18
19 19 defined( 'ABSPATH' ) || exit;
20 20
@@ -20,29 +20,29 @@
20 20
21 21 /**
22 22 * Ability category slug shared by every Copilot ability.
23 23 */
24 -const DESKTOP_MODE_AI_ABILITY_CATEGORY = 'desktop-mode';
24 +const OPENSTATION_AI_ABILITY_CATEGORY = 'openstation';
25 25
26 26 /**
27 - * Registers the `desktop-mode` ability category.
27 + * Registers the `openstation` ability category.
28 28 *
29 29 * @return void
30 30 */
31 -function desktop_mode_ai_register_ability_category() {
31 +function openstation_ai_register_ability_category() {
32 32 if ( ! function_exists( 'wp_register_ability_category' ) ) {
33 33 return;
34 34 }
35 35
36 36 wp_register_ability_category(
37 - DESKTOP_MODE_AI_ABILITY_CATEGORY,
37 + OPENSTATION_AI_ABILITY_CATEGORY,
38 38 array(
39 - 'label' => __( 'Desktop Mode', 'desktop-mode' ),
40 - 'description' => __( 'Read-only content search and wp-admin navigation abilities powering the Desktop Mode AI assistant.', 'desktop-mode' ),
39 + 'label' => __( 'OpenStation', 'desktop-mode' ),
40 + 'description' => __( 'Read-only content search and wp-admin navigation abilities powering the OpenStation AI assistant.', 'desktop-mode' ),
41 41 )
42 42 );
43 43 }
44 -add_action( 'wp_abilities_api_categories_init', 'desktop_mode_ai_register_ability_category' );
44 +add_action( 'wp_abilities_api_categories_init', 'openstation_ai_register_ability_category' );
45 45
46 46 /**
47 47 * The ability names the Copilot offers the model as tools.
48 48 *
@@ -58,9 +58,9 @@
58 58 * site.
59 59 *
60 60 * @return string[] Fully-namespaced ability names.
61 61 */
62 -function desktop_mode_ai_search_ability_names() {
62 +function openstation_ai_search_ability_names() {
63 63 if ( ! function_exists( 'wp_get_abilities' ) ) {
64 64 return array();
65 65 }
66 66
@@ -89,9 +89,9 @@
89 89 *
90 90 * @param string $ability_name Fully-namespaced ability name.
91 91 * @return string
92 92 */
93 -function desktop_mode_ai_ability_tool_name( $ability_name ) {
93 +function openstation_ai_ability_tool_name( $ability_name ) {
94 94 $slug = (string) $ability_name;
95 95 $pos = strpos( $slug, '/' );
96 96 if ( false !== $pos ) {
97 97 $slug = substr( $slug, $pos + 1 );
@@ -111,9 +111,9 @@
111 111 * Copilot's own logged-in requirement.
112 112 *
113 113 * @return bool
114 114 */
115 -function desktop_mode_ai_ability_can_read() {
115 +function openstation_ai_ability_can_read() {
116 116 return is_user_logged_in() && current_user_can( 'read' );
117 117 }
118 118
119 119 /**
@@ -123,9 +123,9 @@
123 123 *
124 124 * @param array<string,array<string,mixed>> $properties Documented top-level props.
125 125 * @return array<string,mixed>
126 126 */
127 -function desktop_mode_ai_ability_output_schema( array $properties = array() ) {
127 +function openstation_ai_ability_output_schema( array $properties = array() ) {
128 128 return array(
129 129 'type' => 'object',
130 130 'additionalProperties' => true,
131 131 // Keep as a plain (associative) array: WordPress's schema validator
@@ -140,9 +140,9 @@
140 140 * Registers every Copilot ability.
141 141 *
142 142 * @return void
143 143 */
144 -function desktop_mode_ai_register_abilities() {
144 +function openstation_ai_register_abilities() {
145 145 if ( ! function_exists( 'wp_register_ability' ) ) {
146 146 return;
147 147 }
148 148
@@ -161,27 +161,48 @@
161 161 ),
162 162 ),
163 163 );
164 164
165 - $search_output = desktop_mode_ai_ability_output_schema(
165 + $search_output = openstation_ai_ability_output_schema(
166 166 array(
167 - 'items' => array( 'type' => 'array', 'description' => 'Matching entities with identity, excerpt, and URLs.' ),
168 - 'count' => array( 'type' => 'integer', 'description' => 'Number of items in this batch.' ),
169 - 'total' => array( 'type' => 'integer', 'description' => 'Total matches across all batches.' ),
170 - 'has_more' => array( 'type' => 'boolean', 'description' => 'Whether another batch is available at the next offset.' ),
167 + 'items' => array(
168 + 'type' => 'array',
169 + 'description' => 'Matching entities with identity, excerpt, and URLs.',
170 + ),
171 + 'count' => array(
172 + 'type' => 'integer',
173 + 'description' => 'Number of items in this batch.',
174 + ),
175 + 'total' => array(
176 + 'type' => 'integer',
177 + 'description' => 'Total matches across all batches.',
178 + ),
179 + 'has_more' => array(
180 + 'type' => 'boolean',
181 + 'description' => 'Whether another batch is available at the next offset.',
182 + ),
171 183 )
172 184 );
173 185
174 186 $readonly_meta = array(
175 - 'annotations' => array( 'readonly' => true, 'idempotent' => true ),
187 + 'annotations' => array(
188 + 'readonly' => true,
189 + 'idempotent' => true,
190 + ),
176 191 'show_in_rest' => true,
177 - 'mcp' => array( 'public' => true, 'type' => 'tool' ),
192 + 'mcp' => array(
193 + 'public' => true,
194 + 'type' => 'tool',
195 + ),
178 196 );
179 197
180 198 // Admin-only abilities are still read-only, but must not be exposed to
181 199 // external agents over MCP.
182 200 $readonly_private_meta = array(
183 - 'annotations' => array( 'readonly' => true, 'idempotent' => true ),
201 + 'annotations' => array(
202 + 'readonly' => true,
203 + 'idempotent' => true,
204 + ),
184 205 'show_in_rest' => true,
185 206 );
186 207
187 208 wp_register_ability(
@@ -188,15 +209,15 @@
188 209 'desktop-mode/search-posts',
189 210 array(
190 211 'label' => __( 'Search posts', 'desktop-mode' ),
191 212 'description' => 'Keyword-searches published WordPress blog posts by title and content (WordPress native search). Use this when the user is looking for content they or someone else wrote as a post or article. Pass the key search terms as `query`. Returns up to 10 matching posts with their title, a content excerpt, date, and URLs. If has_more is true, call again with the next offset.',
192 - 'category' => DESKTOP_MODE_AI_ABILITY_CATEGORY,
213 + 'category' => OPENSTATION_AI_ABILITY_CATEGORY,
193 214 'input_schema' => $query_offset_input,
194 215 'output_schema' => $search_output,
195 216 'execute_callback' => static function ( $input ) {
196 - return desktop_mode_ai_search_dispatch_tool( 'search_posts', (array) $input );
217 + return openstation_ai_search_dispatch_tool( 'search_posts', (array) $input );
197 218 },
198 - 'permission_callback' => 'desktop_mode_ai_ability_can_read',
219 + 'permission_callback' => 'openstation_ai_ability_can_read',
199 220 'meta' => $readonly_meta,
200 221 )
201 222 );
202 223
@@ -204,15 +225,15 @@
204 225 'desktop-mode/search-pages',
205 226 array(
206 227 'label' => __( 'Search pages', 'desktop-mode' ),
207 228 'description' => 'Keyword-searches published WordPress pages (About, Contact, Services, Portfolio, etc.) by title and content. Use this when the user is looking for a static page, landing page, or informational page on the site. Pass the key search terms as `query`. Returns up to 10 matching pages with their title, a content excerpt, and URLs. If has_more is true, call again with the next offset.',
208 - 'category' => DESKTOP_MODE_AI_ABILITY_CATEGORY,
229 + 'category' => OPENSTATION_AI_ABILITY_CATEGORY,
209 230 'input_schema' => $query_offset_input,
210 231 'output_schema' => $search_output,
211 232 'execute_callback' => static function ( $input ) {
212 - return desktop_mode_ai_search_dispatch_tool( 'search_pages', (array) $input );
233 + return openstation_ai_search_dispatch_tool( 'search_pages', (array) $input );
213 234 },
214 - 'permission_callback' => 'desktop_mode_ai_ability_can_read',
235 + 'permission_callback' => 'openstation_ai_ability_can_read',
215 236 'meta' => $readonly_meta,
216 237 )
217 238 );
218 239
@@ -219,16 +240,16 @@
219 240 wp_register_ability(
220 241 'desktop-mode/search-comments',
221 242 array(
222 243 'label' => __( 'Search comments', 'desktop-mode' ),
223 - 'description' => 'Keyword-searches approved WordPress comments across ALL posts by their text (WordPress native search). Use this when the user remembers something a reader said but does not know which post it was on. Pass the distinctive words from the comment as `query`. Returns up to 10 matching comments with an excerpt, parent post title, and URLs. If has_more is true, call again with the next offset.',
224 - 'category' => DESKTOP_MODE_AI_ABILITY_CATEGORY,
244 + 'description' => 'Keyword-searches approved WordPress comments by their text (WordPress native search), across all posts the requesting user is allowed to read — comments on private, draft, or password-protected posts the user cannot access are excluded. Use this when the user remembers something a reader said but does not know which post it was on. Pass the distinctive words from the comment as `query`. Returns up to 10 matching comments with an excerpt, parent post title, and URLs. If has_more is true, call again with the next offset.',
245 + 'category' => OPENSTATION_AI_ABILITY_CATEGORY,
225 246 'input_schema' => $query_offset_input,
226 247 'output_schema' => $search_output,
227 248 'execute_callback' => static function ( $input ) {
228 - return desktop_mode_ai_search_dispatch_tool( 'search_comments', (array) $input );
249 + return openstation_ai_search_dispatch_tool( 'search_comments', (array) $input );
229 250 },
230 - 'permission_callback' => 'desktop_mode_ai_ability_can_read',
251 + 'permission_callback' => 'openstation_ai_ability_can_read',
231 252 'meta' => $readonly_meta,
232 253 )
233 254 );
234 255
@@ -235,10 +256,10 @@
235 256 wp_register_ability(
236 257 'desktop-mode/search-comments-by-post',
237 258 array(
238 259 'label' => __( 'Search comments on a post', 'desktop-mode' ),
239 - 'description' => 'Keyword-searches approved comments on a SPECIFIC post by its WordPress ID. Use this when you have already identified a post (via search-posts) and the user\'s query also mentions something a reader said on that post — e.g. "I remember a comment on my Málaga post asking about the Alcazaba at night." Call search-posts first to find the post ID, then call this tool with that ID and the distinctive words as `query`. Much more precise than search-comments when the parent post is known. If has_more is true, call again with the next offset.',
240 - 'category' => DESKTOP_MODE_AI_ABILITY_CATEGORY,
260 + 'description' => 'Keyword-searches approved comments on a SPECIFIC post by its WordPress ID (the post must be readable by the requesting user; an unreadable or nonexistent post returns an empty result). Use this when you have already identified a post (via search-posts) and the user\'s query also mentions something a reader said on that post — e.g. "I remember a comment on my Málaga post asking about the Alcazaba at night." Call search-posts first to find the post ID, then call this tool with that ID and the distinctive words as `query`. Much more precise than search-comments when the parent post is known. If has_more is true, call again with the next offset.',
261 + 'category' => OPENSTATION_AI_ABILITY_CATEGORY,
241 262 'input_schema' => array(
242 263 'type' => 'object',
243 264 'additionalProperties' => false,
244 265 'required' => array( 'post_id', 'query', 'offset' ),
@@ -258,11 +279,11 @@
258 279 ),
259 280 ),
260 281 'output_schema' => $search_output,
261 282 'execute_callback' => static function ( $input ) {
262 - return desktop_mode_ai_search_dispatch_tool( 'search_comments_by_post', (array) $input );
283 + return openstation_ai_search_dispatch_tool( 'search_comments_by_post', (array) $input );
263 284 },
264 - 'permission_callback' => 'desktop_mode_ai_ability_can_read',
285 + 'permission_callback' => 'openstation_ai_ability_can_read',
265 286 'meta' => $readonly_meta,
266 287 )
267 288 );
268 289
@@ -276,9 +297,9 @@
276 297 // its own system prompt and answer schema. Naming those
277 298 // fields here would instruct an agent to emit a shape its
278 299 // answer schema does not have.
279 300 'description' => 'Returns the full catalog of WordPress admin (wp-admin) destinations — pages for managing posts, categories, users, plugins, themes, settings, etc. Call this when the user asks "where can I find X?", "how do I get to Y?", "where are the settings for Z?" — any navigational question about the admin UI. Each entry carries a title, url, icon, and description, so pick the few most relevant to the query. The catalog is small and stable so one call is enough.',
280 - 'category' => DESKTOP_MODE_AI_ABILITY_CATEGORY,
301 + 'category' => OPENSTATION_AI_ABILITY_CATEGORY,
281 302 'input_schema' => array(
282 303 'type' => 'object',
283 304 'additionalProperties' => false,
284 305 'required' => array(),
@@ -283,15 +304,20 @@
283 304 'additionalProperties' => false,
284 305 'required' => array(),
285 306 'properties' => (object) array(),
286 307 ),
287 - 'output_schema' => desktop_mode_ai_ability_output_schema(
288 - array( 'pages' => array( 'type' => 'array', 'description' => 'Admin destinations with title/url/icon/description.' ) )
308 + 'output_schema' => openstation_ai_ability_output_schema(
309 + array(
310 + 'pages' => array(
311 + 'type' => 'array',
312 + 'description' => 'Admin destinations with title/url/icon/description.',
313 + ),
314 + )
289 315 ),
290 316 'execute_callback' => static function ( $input ) {
291 - return desktop_mode_ai_search_dispatch_tool( 'list_admin_pages', (array) $input );
317 + return openstation_ai_search_dispatch_tool( 'list_admin_pages', (array) $input );
292 318 },
293 - 'permission_callback' => 'desktop_mode_ai_ability_can_read',
319 + 'permission_callback' => 'openstation_ai_ability_can_read',
294 320 'meta' => $readonly_meta,
295 321 )
296 322 );
297 323
@@ -299,9 +325,9 @@
299 325 'desktop-mode/search-wporg-plugins',
300 326 array(
301 327 'label' => __( 'Search WordPress.org plugins', 'desktop-mode' ),
302 328 'description' => 'Searches the official WordPress.org plugin directory. Use this when the user asks for a plugin recommendation — e.g. "is there a plugin for SEO?", "find me a backup plugin", "a caching plugin", "form builder". Returns up to 10 plugins with name, description, rating, active install count, and an admin URL that opens the plugin-info / install screen directly.',
303 - 'category' => DESKTOP_MODE_AI_ABILITY_CATEGORY,
329 + 'category' => OPENSTATION_AI_ABILITY_CATEGORY,
304 330 'input_schema' => array(
305 331 'type' => 'object',
306 332 'additionalProperties' => false,
307 333 'required' => array( 'query' ),
@@ -311,18 +337,21 @@
311 337 'description' => 'Plain-language search terms — e.g. "seo", "backup", "caching", "woocommerce", "contact form".',
312 338 ),
313 339 ),
314 340 ),
315 - 'output_schema' => desktop_mode_ai_ability_output_schema(
341 + 'output_schema' => openstation_ai_ability_output_schema(
316 342 array(
317 - 'results' => array( 'type' => 'array', 'description' => 'Matching plugins with name, description, rating, installs, and admin URL.' ),
343 + 'results' => array(
344 + 'type' => 'array',
345 + 'description' => 'Matching plugins with name, description, rating, installs, and admin URL.',
346 + ),
318 347 'count' => array( 'type' => 'integer' ),
319 348 )
320 349 ),
321 350 'execute_callback' => static function ( $input ) {
322 - return desktop_mode_ai_search_dispatch_tool( 'search_wporg_plugins', (array) $input );
351 + return openstation_ai_search_dispatch_tool( 'search_wporg_plugins', (array) $input );
323 352 },
324 - 'permission_callback' => 'desktop_mode_ai_ability_can_read',
353 + 'permission_callback' => 'openstation_ai_ability_can_read',
325 354 'meta' => $readonly_meta,
326 355 )
327 356 );
328 357
@@ -330,9 +359,9 @@
330 359 'desktop-mode/get-php-error-log',
331 360 array(
332 361 'label' => __( 'Read PHP error log', 'desktop-mode' ),
333 362 'description' => 'Reads the most recent entries from the site\'s PHP error log — typically wp-content/debug.log when WP_DEBUG_LOG is enabled, or the path set by the PHP error_log directive. Use this when the user asks "are there any errors?", "check the logs", "what went wrong?", or is troubleshooting a white screen / 500. Each entry is parsed into { timestamp, level, message } so you can summarise them. Administrators only.',
334 - 'category' => DESKTOP_MODE_AI_ABILITY_CATEGORY,
363 + 'category' => OPENSTATION_AI_ABILITY_CATEGORY,
335 364 'input_schema' => array(
336 365 'type' => 'object',
337 366 'additionalProperties' => false,
338 367 'required' => array( 'lines' ),
@@ -342,16 +371,19 @@
342 371 'description' => 'How many recent log lines to return (1-500). Use 20-50 for a quick look, 100-200 for wider context.',
343 372 ),
344 373 ),
345 374 ),
346 - 'output_schema' => desktop_mode_ai_ability_output_schema(
375 + 'output_schema' => openstation_ai_ability_output_schema(
347 376 array(
348 377 'log_available' => array( 'type' => 'boolean' ),
349 - 'entries' => array( 'type' => 'array', 'description' => 'Parsed log lines: { timestamp, level, message }.' ),
378 + 'entries' => array(
379 + 'type' => 'array',
380 + 'description' => 'Parsed log lines: { timestamp, level, message }.',
381 + ),
350 382 )
351 383 ),
352 384 'execute_callback' => static function ( $input ) {
353 - return desktop_mode_ai_search_dispatch_tool( 'get_php_error_log', (array) $input );
385 + return openstation_ai_search_dispatch_tool( 'get_php_error_log', (array) $input );
354 386 },
355 387 // Admin-only — mirrors the previous in-dispatcher manage_options gate.
356 388 'permission_callback' => static function () {
357 389 return current_user_can( 'manage_options' );
@@ -359,29 +391,32 @@
359 391 'meta' => $readonly_private_meta,
360 392 )
361 393 );
362 394
363 - desktop_mode_ai_register_comment_analysis_ability();
395 + openstation_ai_register_comment_analysis_ability();
364 396 }
365 -add_action( 'wp_abilities_api_init', 'desktop_mode_ai_register_abilities' );
397 +add_action( 'wp_abilities_api_init', 'openstation_ai_register_abilities' );
366 398
367 399 /**
368 400 * Registers the comment-spam analysis ability.
369 401 *
370 402 * Not offered to the model during a search turn (see
371 - * {@see desktop_mode_ai_search_ability_names()}); the moderation pipeline
372 - * resolves and executes it directly ({@see desktop_mode_ai_analyze_comment_now()}
373 - * runs through it). Exposed in the abilities catalog for observability + reuse.
403 + * {@see openstation_ai_search_ability_names()}) — a search turn can be driven
404 + * by attacker-controlled comment text, and this one spends provider tokens.
405 + * It is an on-demand ability: a caller with `moderate_comments` runs it for one
406 + * comment and gets the verdict back. Automatic scoring on comment save was
407 + * removed (`docs/migration-comments-ai-scoring.md`), so nothing in the plugin
408 + * invokes it on its own.
374 409 *
375 410 * @return void
376 411 */
377 -function desktop_mode_ai_register_comment_analysis_ability() {
412 +function openstation_ai_register_comment_analysis_ability() {
378 413 wp_register_ability(
379 414 'desktop-mode/analyze-comment',
380 415 array(
381 416 'label' => __( 'Analyze comment for spam', 'desktop-mode' ),
382 - 'description' => 'Runs the AI spam/harm analysis for a single comment and returns its structured verdict ({ topic, ai_summary, harmful, spam }). Used by comment moderation to score incoming comments.',
383 - 'category' => DESKTOP_MODE_AI_ABILITY_CATEGORY,
417 + 'description' => 'Runs the AI spam/harm analysis for a single comment and returns its structured verdict ({ topic, ai_summary, harmful, spam }). On-demand only: nothing in the plugin runs it automatically.',
418 + 'category' => OPENSTATION_AI_ABILITY_CATEGORY,
384 419 'input_schema' => array(
385 420 'type' => 'object',
386 421 'additionalProperties' => false,
387 422 'required' => array( 'comment_id' ),
@@ -391,9 +426,9 @@
391 426 'description' => 'The WordPress ID of the comment to analyze.',
392 427 ),
393 428 ),
394 429 ),
395 - 'output_schema' => desktop_mode_ai_ability_output_schema(
430 + 'output_schema' => openstation_ai_ability_output_schema(
396 431 array(
397 432 'topic' => array( 'type' => 'string' ),
398 433 'ai_summary' => array( 'type' => 'string' ),
399 434 'harmful' => array( 'type' => 'boolean' ),
@@ -399,14 +434,17 @@
399 434 'harmful' => array( 'type' => 'boolean' ),
400 435 'spam' => array( 'type' => 'boolean' ),
401 436 )
402 437 ),
403 - 'execute_callback' => 'desktop_mode_ai_ability_analyze_comment',
438 + 'execute_callback' => 'openstation_ai_ability_analyze_comment',
404 439 'permission_callback' => static function () {
405 440 return current_user_can( 'moderate_comments' );
406 441 },
407 442 'meta' => array(
408 - 'annotations' => array( 'readonly' => true, 'idempotent' => true ),
443 + 'annotations' => array(
444 + 'readonly' => true,
445 + 'idempotent' => true,
446 + ),
409 447 'show_in_rest' => true,
410 448 ),
411 449 )
412 450 );
@@ -417,13 +455,13 @@
417 455 *
418 456 * @param array<string,mixed> $input Validated input (`comment_id`).
419 457 * @return array|WP_Error Structured verdict, or an error.
420 458 */
421 -function desktop_mode_ai_ability_analyze_comment( $input ) {
459 +function openstation_ai_ability_analyze_comment( $input ) {
422 460 $comment_id = isset( $input['comment_id'] ) ? (int) $input['comment_id'] : 0;
423 461 $comment = $comment_id > 0 ? get_comment( $comment_id ) : null;
424 462 if ( ! $comment instanceof WP_Comment ) {
425 - return new WP_Error( 'desktop_mode_ai_comment_not_found', __( 'Comment not found.', 'desktop-mode' ) );
463 + return new WP_Error( 'openstation_ai_comment_not_found', __( 'Comment not found.', 'desktop-mode' ) );
426 464 }
427 465
428 - return desktop_mode_ai_analyze_comment_now( $comment, (int) $comment->user_id );
466 + return openstation_ai_analyze_comment_now( $comment, (int) $comment->user_id );
429 467 }