PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.13
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.13
1.1.13 1.1.12 1.1.11 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 All 37 releases
← All changes | includes/ai-copilot/abilities.php +152 -69 0.9.8 → 1.1.13 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 );
@@ -104,8 +104,53 @@
104 104 return trim( (string) $slug, '_' );
105 105 }
106 106
107 107 /**
108 + * Maps abilities to model-facing tool names that are unique within one
109 + * request.
110 + *
111 + * Stripping the namespace means two plugins can land on the same name
112 + * (`desktop-mode/get-site-context` and `acme/get-site-context`), and
113 + * providers reject the whole request when tool names repeat ("Tool names
114 + * must be unique"). OpenStation's own abilities claim their bare names
115 + * first, because the system prompt and progress labels refer to those.
116 + * Any later ability whose bare name is taken keeps its namespace
117 + * (`acme_get_site_context`) so it stays callable; one that still
118 + * collides is dropped.
119 + *
120 + * @param string[] $ability_names Fully-namespaced ability names.
121 + * @return array<string,string> Tool name => ability name, own abilities first.
122 + */
123 +function openstation_ai_unique_ability_tool_names( array $ability_names ) {
124 + $own = array();
125 + $others = array();
126 + foreach ( $ability_names as $ability_name ) {
127 + $ability_name = (string) $ability_name;
128 + if ( 0 === strpos( $ability_name, 'desktop-mode/' ) ) {
129 + $own[] = $ability_name;
130 + } else {
131 + $others[] = $ability_name;
132 + }
133 + }
134 +
135 + $map = array();
136 + foreach ( array_merge( $own, $others ) as $ability_name ) {
137 + $candidates = array(
138 + openstation_ai_ability_tool_name( $ability_name ),
139 + openstation_ai_ability_tool_name( str_replace( '/', '_', $ability_name ) ),
140 + );
141 + foreach ( $candidates as $tool_name ) {
142 + if ( '' !== $tool_name && ! isset( $map[ $tool_name ] ) ) {
143 + $map[ $tool_name ] = $ability_name;
144 + break;
145 + }
146 + }
147 + }
148 +
149 + return $map;
150 +}
151 +
152 +/**
108 153 * Permission callback: any logged-in user who can read the site.
109 154 *
110 155 * Mirrors the read-only search/navigation tools, which were ungated beyond the
111 156 * Copilot's own logged-in requirement.
@@ -111,9 +156,9 @@
111 156 * Copilot's own logged-in requirement.
112 157 *
113 158 * @return bool
114 159 */
115 -function desktop_mode_ai_ability_can_read() {
160 +function openstation_ai_ability_can_read() {
116 161 return is_user_logged_in() && current_user_can( 'read' );
117 162 }
118 163
119 164 /**
@@ -123,9 +168,9 @@
123 168 *
124 169 * @param array<string,array<string,mixed>> $properties Documented top-level props.
125 170 * @return array<string,mixed>
126 171 */
127 -function desktop_mode_ai_ability_output_schema( array $properties = array() ) {
172 +function openstation_ai_ability_output_schema( array $properties = array() ) {
128 173 return array(
129 174 'type' => 'object',
130 175 'additionalProperties' => true,
131 176 // Keep as a plain (associative) array: WordPress's schema validator
@@ -140,9 +185,9 @@
140 185 * Registers every Copilot ability.
141 186 *
142 187 * @return void
143 188 */
144 -function desktop_mode_ai_register_abilities() {
189 +function openstation_ai_register_abilities() {
145 190 if ( ! function_exists( 'wp_register_ability' ) ) {
146 191 return;
147 192 }
148 193
@@ -152,9 +197,9 @@
152 197 'required' => array( 'query', 'offset' ),
153 198 'properties' => array(
154 199 'query' => array(
155 200 'type' => 'string',
156 - 'description' => 'Keyword search terms matched against the title and content (WordPress native search). Distil the user\'s request to the essential nouns — e.g. for "that post I wrote about making paella" pass "paella". Avoid stop-words and full sentences.',
201 + 'description' => 'Keyword search terms matched against the title and content (WordPress native search). Distil the user\'s request to the essential nouns — e.g. for "that post I wrote about making paella" pass "paella". Avoid stop-words and full sentences. An empty string lists everything, newest first — only for questions about who, how many or the latest, never to look for a topic.',
157 202 ),
158 203 'offset' => array(
159 204 'type' => 'integer',
160 205 'description' => 'Zero-based starting position. Use 0 for the first batch, 10 for the second, and so on.',
@@ -161,27 +206,48 @@
161 206 ),
162 207 ),
163 208 );
164 209
165 - $search_output = desktop_mode_ai_ability_output_schema(
210 + $search_output = openstation_ai_ability_output_schema(
166 211 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.' ),
212 + 'items' => array(
213 + 'type' => 'array',
214 + 'description' => 'Matching entities with identity, excerpt, and URLs.',
215 + ),
216 + 'count' => array(
217 + 'type' => 'integer',
218 + 'description' => 'Number of items in this batch.',
219 + ),
220 + 'total' => array(
221 + 'type' => 'integer',
222 + 'description' => 'Total matches across all batches.',
223 + ),
224 + 'has_more' => array(
225 + 'type' => 'boolean',
226 + 'description' => 'Whether another batch is available at the next offset.',
227 + ),
171 228 )
172 229 );
173 230
174 231 $readonly_meta = array(
175 - 'annotations' => array( 'readonly' => true, 'idempotent' => true ),
232 + 'annotations' => array(
233 + 'readonly' => true,
234 + 'idempotent' => true,
235 + ),
176 236 'show_in_rest' => true,
177 - 'mcp' => array( 'public' => true, 'type' => 'tool' ),
237 + 'mcp' => array(
238 + 'public' => true,
239 + 'type' => 'tool',
240 + ),
178 241 );
179 242
180 243 // Admin-only abilities are still read-only, but must not be exposed to
181 244 // external agents over MCP.
182 245 $readonly_private_meta = array(
183 - 'annotations' => array( 'readonly' => true, 'idempotent' => true ),
246 + 'annotations' => array(
247 + 'readonly' => true,
248 + 'idempotent' => true,
249 + ),
184 250 'show_in_rest' => true,
185 251 );
186 252
187 253 wp_register_ability(
@@ -187,16 +253,16 @@
187 253 wp_register_ability(
188 254 'desktop-mode/search-posts',
189 255 array(
190 256 'label' => __( 'Search posts', 'desktop-mode' ),
191 - '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,
257 + '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`. Only for listing or counting questions ("how many posts do I have?") pass an empty `query`, which lists the newest posts (`total` is the full count). 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.',
258 + 'category' => OPENSTATION_AI_ABILITY_CATEGORY,
193 259 'input_schema' => $query_offset_input,
194 260 'output_schema' => $search_output,
195 261 'execute_callback' => static function ( $input ) {
196 - return desktop_mode_ai_search_dispatch_tool( 'search_posts', (array) $input );
262 + return openstation_ai_search_dispatch_tool( 'search_posts', (array) $input );
197 263 },
198 - 'permission_callback' => 'desktop_mode_ai_ability_can_read',
264 + 'permission_callback' => 'openstation_ai_ability_can_read',
199 265 'meta' => $readonly_meta,
200 266 )
201 267 );
202 268
@@ -204,15 +270,15 @@
204 270 'desktop-mode/search-pages',
205 271 array(
206 272 'label' => __( 'Search pages', 'desktop-mode' ),
207 273 '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,
274 + 'category' => OPENSTATION_AI_ABILITY_CATEGORY,
209 275 'input_schema' => $query_offset_input,
210 276 'output_schema' => $search_output,
211 277 'execute_callback' => static function ( $input ) {
212 - return desktop_mode_ai_search_dispatch_tool( 'search_pages', (array) $input );
278 + return openstation_ai_search_dispatch_tool( 'search_pages', (array) $input );
213 279 },
214 - 'permission_callback' => 'desktop_mode_ai_ability_can_read',
280 + 'permission_callback' => 'openstation_ai_ability_can_read',
215 281 'meta' => $readonly_meta,
216 282 )
217 283 );
218 284
@@ -219,16 +285,16 @@
219 285 wp_register_ability(
220 286 'desktop-mode/search-comments',
221 287 array(
222 288 '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,
289 + '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`. Also use it, with an empty `query`, for questions about the comments themselves — who has commented, the latest comments, how many there are (`total` is the full count). Returns up to 10 matching comments with the commenter\'s name (`author_name`), an excerpt, parent post title, and URLs. If has_more is true, call again with the next offset.',
290 + 'category' => OPENSTATION_AI_ABILITY_CATEGORY,
225 291 'input_schema' => $query_offset_input,
226 292 'output_schema' => $search_output,
227 293 'execute_callback' => static function ( $input ) {
228 - return desktop_mode_ai_search_dispatch_tool( 'search_comments', (array) $input );
294 + return openstation_ai_search_dispatch_tool( 'search_comments', (array) $input );
229 295 },
230 - 'permission_callback' => 'desktop_mode_ai_ability_can_read',
296 + 'permission_callback' => 'openstation_ai_ability_can_read',
231 297 'meta' => $readonly_meta,
232 298 )
233 299 );
234 300
@@ -235,10 +301,10 @@
235 301 wp_register_ability(
236 302 'desktop-mode/search-comments-by-post',
237 303 array(
238 304 '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,
305 + '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 the user describes both a post and something a reader said on it: identify the post with search-posts, then pass its ID and the distinctive words of the comment as `query`. More precise than search-comments whenever the parent post is known. Each match carries the commenter\'s name (`author_name`) and an excerpt. If has_more is true, call again with the next offset.',
306 + 'category' => OPENSTATION_AI_ABILITY_CATEGORY,
241 307 'input_schema' => array(
242 308 'type' => 'object',
243 309 'additionalProperties' => false,
244 310 'required' => array( 'post_id', 'query', 'offset' ),
@@ -258,11 +324,11 @@
258 324 ),
259 325 ),
260 326 'output_schema' => $search_output,
261 327 'execute_callback' => static function ( $input ) {
262 - return desktop_mode_ai_search_dispatch_tool( 'search_comments_by_post', (array) $input );
328 + return openstation_ai_search_dispatch_tool( 'search_comments_by_post', (array) $input );
263 329 },
264 - 'permission_callback' => 'desktop_mode_ai_ability_can_read',
330 + 'permission_callback' => 'openstation_ai_ability_can_read',
265 331 'meta' => $readonly_meta,
266 332 )
267 333 );
268 334
@@ -276,9 +342,9 @@
276 342 // its own system prompt and answer schema. Naming those
277 343 // fields here would instruct an agent to emit a shape its
278 344 // answer schema does not have.
279 345 '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,
346 + 'category' => OPENSTATION_AI_ABILITY_CATEGORY,
281 347 'input_schema' => array(
282 348 'type' => 'object',
283 349 'additionalProperties' => false,
284 350 'required' => array(),
@@ -283,15 +349,20 @@
283 349 'additionalProperties' => false,
284 350 'required' => array(),
285 351 'properties' => (object) array(),
286 352 ),
287 - 'output_schema' => desktop_mode_ai_ability_output_schema(
288 - array( 'pages' => array( 'type' => 'array', 'description' => 'Admin destinations with title/url/icon/description.' ) )
353 + 'output_schema' => openstation_ai_ability_output_schema(
354 + array(
355 + 'pages' => array(
356 + 'type' => 'array',
357 + 'description' => 'Admin destinations with title/url/icon/description.',
358 + ),
359 + )
289 360 ),
290 361 'execute_callback' => static function ( $input ) {
291 - return desktop_mode_ai_search_dispatch_tool( 'list_admin_pages', (array) $input );
362 + return openstation_ai_search_dispatch_tool( 'list_admin_pages', (array) $input );
292 363 },
293 - 'permission_callback' => 'desktop_mode_ai_ability_can_read',
364 + 'permission_callback' => 'openstation_ai_ability_can_read',
294 365 'meta' => $readonly_meta,
295 366 )
296 367 );
297 368
@@ -298,10 +369,10 @@
298 369 wp_register_ability(
299 370 'desktop-mode/search-wporg-plugins',
300 371 array(
301 372 'label' => __( 'Search WordPress.org plugins', 'desktop-mode' ),
302 - '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,
373 + '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 (0-100) and stars (0-5), active install count, and an admin URL that opens the plugin-info / install screen directly.',
374 + 'category' => OPENSTATION_AI_ABILITY_CATEGORY,
304 375 'input_schema' => array(
305 376 'type' => 'object',
306 377 'additionalProperties' => false,
307 378 'required' => array( 'query' ),
@@ -311,18 +382,21 @@
311 382 'description' => 'Plain-language search terms — e.g. "seo", "backup", "caching", "woocommerce", "contact form".',
312 383 ),
313 384 ),
314 385 ),
315 - 'output_schema' => desktop_mode_ai_ability_output_schema(
386 + 'output_schema' => openstation_ai_ability_output_schema(
316 387 array(
317 - 'results' => array( 'type' => 'array', 'description' => 'Matching plugins with name, description, rating, installs, and admin URL.' ),
388 + 'results' => array(
389 + 'type' => 'array',
390 + 'description' => 'Matching plugins with name, description, rating (0-100), stars (0-5), installs, and admin URL.',
391 + ),
318 392 'count' => array( 'type' => 'integer' ),
319 393 )
320 394 ),
321 395 'execute_callback' => static function ( $input ) {
322 - return desktop_mode_ai_search_dispatch_tool( 'search_wporg_plugins', (array) $input );
396 + return openstation_ai_search_dispatch_tool( 'search_wporg_plugins', (array) $input );
323 397 },
324 - 'permission_callback' => 'desktop_mode_ai_ability_can_read',
398 + 'permission_callback' => 'openstation_ai_ability_can_read',
325 399 'meta' => $readonly_meta,
326 400 )
327 401 );
328 402
@@ -330,9 +404,9 @@
330 404 'desktop-mode/get-php-error-log',
331 405 array(
332 406 'label' => __( 'Read PHP error log', 'desktop-mode' ),
333 407 '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,
408 + 'category' => OPENSTATION_AI_ABILITY_CATEGORY,
335 409 'input_schema' => array(
336 410 'type' => 'object',
337 411 'additionalProperties' => false,
338 412 'required' => array( 'lines' ),
@@ -342,16 +416,19 @@
342 416 'description' => 'How many recent log lines to return (1-500). Use 20-50 for a quick look, 100-200 for wider context.',
343 417 ),
344 418 ),
345 419 ),
346 - 'output_schema' => desktop_mode_ai_ability_output_schema(
420 + 'output_schema' => openstation_ai_ability_output_schema(
347 421 array(
348 422 'log_available' => array( 'type' => 'boolean' ),
349 - 'entries' => array( 'type' => 'array', 'description' => 'Parsed log lines: { timestamp, level, message }.' ),
423 + 'entries' => array(
424 + 'type' => 'array',
425 + 'description' => 'Parsed log lines: { timestamp, level, message }.',
426 + ),
350 427 )
351 428 ),
352 429 'execute_callback' => static function ( $input ) {
353 - return desktop_mode_ai_search_dispatch_tool( 'get_php_error_log', (array) $input );
430 + return openstation_ai_search_dispatch_tool( 'get_php_error_log', (array) $input );
354 431 },
355 432 // Admin-only — mirrors the previous in-dispatcher manage_options gate.
356 433 'permission_callback' => static function () {
357 434 return current_user_can( 'manage_options' );
@@ -359,29 +436,32 @@
359 436 'meta' => $readonly_private_meta,
360 437 )
361 438 );
362 439
363 - desktop_mode_ai_register_comment_analysis_ability();
440 + openstation_ai_register_comment_analysis_ability();
364 441 }
365 -add_action( 'wp_abilities_api_init', 'desktop_mode_ai_register_abilities' );
442 +add_action( 'wp_abilities_api_init', 'openstation_ai_register_abilities' );
366 443
367 444 /**
368 445 * Registers the comment-spam analysis ability.
369 446 *
370 447 * 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.
448 + * {@see openstation_ai_search_ability_names()}) — a search turn can be driven
449 + * by attacker-controlled comment text, and this one spends provider tokens.
450 + * It is an on-demand ability: a caller with `moderate_comments` runs it for one
451 + * comment and gets the verdict back. Automatic scoring on comment save was
452 + * removed (`docs/migration-comments-ai-scoring.md`), so nothing in the plugin
453 + * invokes it on its own.
374 454 *
375 455 * @return void
376 456 */
377 -function desktop_mode_ai_register_comment_analysis_ability() {
457 +function openstation_ai_register_comment_analysis_ability() {
378 458 wp_register_ability(
379 459 'desktop-mode/analyze-comment',
380 460 array(
381 461 '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,
462 + '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.',
463 + 'category' => OPENSTATION_AI_ABILITY_CATEGORY,
384 464 'input_schema' => array(
385 465 'type' => 'object',
386 466 'additionalProperties' => false,
387 467 'required' => array( 'comment_id' ),
@@ -391,9 +471,9 @@
391 471 'description' => 'The WordPress ID of the comment to analyze.',
392 472 ),
393 473 ),
394 474 ),
395 - 'output_schema' => desktop_mode_ai_ability_output_schema(
475 + 'output_schema' => openstation_ai_ability_output_schema(
396 476 array(
397 477 'topic' => array( 'type' => 'string' ),
398 478 'ai_summary' => array( 'type' => 'string' ),
399 479 'harmful' => array( 'type' => 'boolean' ),
@@ -399,14 +479,17 @@
399 479 'harmful' => array( 'type' => 'boolean' ),
400 480 'spam' => array( 'type' => 'boolean' ),
401 481 )
402 482 ),
403 - 'execute_callback' => 'desktop_mode_ai_ability_analyze_comment',
483 + 'execute_callback' => 'openstation_ai_ability_analyze_comment',
404 484 'permission_callback' => static function () {
405 485 return current_user_can( 'moderate_comments' );
406 486 },
407 487 'meta' => array(
408 - 'annotations' => array( 'readonly' => true, 'idempotent' => true ),
488 + 'annotations' => array(
489 + 'readonly' => true,
490 + 'idempotent' => true,
491 + ),
409 492 'show_in_rest' => true,
410 493 ),
411 494 )
412 495 );
@@ -417,13 +500,13 @@
417 500 *
418 501 * @param array<string,mixed> $input Validated input (`comment_id`).
419 502 * @return array|WP_Error Structured verdict, or an error.
420 503 */
421 -function desktop_mode_ai_ability_analyze_comment( $input ) {
504 +function openstation_ai_ability_analyze_comment( $input ) {
422 505 $comment_id = isset( $input['comment_id'] ) ? (int) $input['comment_id'] : 0;
423 506 $comment = $comment_id > 0 ? get_comment( $comment_id ) : null;
424 507 if ( ! $comment instanceof WP_Comment ) {
425 - return new WP_Error( 'desktop_mode_ai_comment_not_found', __( 'Comment not found.', 'desktop-mode' ) );
508 + return new WP_Error( 'openstation_ai_comment_not_found', __( 'Comment not found.', 'desktop-mode' ) );
426 509 }
427 510
428 - return desktop_mode_ai_analyze_comment_now( $comment, (int) $comment->user_id );
511 + return openstation_ai_analyze_comment_now( $comment, (int) $comment->user_id );
429 512 }