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 +159 -86 0.9.6 → 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,31 +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 - * @since 0.9.4
30 - *
31 29 * @return void
32 30 */
33 -function desktop_mode_ai_register_ability_category() {
31 +function openstation_ai_register_ability_category() {
34 32 if ( ! function_exists( 'wp_register_ability_category' ) ) {
35 33 return;
36 34 }
37 35
38 36 wp_register_ability_category(
39 - DESKTOP_MODE_AI_ABILITY_CATEGORY,
37 + OPENSTATION_AI_ABILITY_CATEGORY,
40 38 array(
41 - 'label' => __( 'Desktop Mode', 'desktop-mode' ),
42 - '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' ),
43 41 )
44 42 );
45 43 }
46 -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' );
47 45
48 46 /**
49 47 * The ability names the Copilot offers the model as tools.
50 48 *
@@ -58,13 +56,11 @@
58 56 * driven by attacker-controlled content (comment / post text that lands in a
59 57 * tool result), so the model is never handed an ability that could change the
60 58 * site.
61 59 *
62 - * @since 0.9.4
63 - *
64 60 * @return string[] Fully-namespaced ability names.
65 61 */
66 -function desktop_mode_ai_search_ability_names() {
62 +function openstation_ai_search_ability_names() {
67 63 if ( ! function_exists( 'wp_get_abilities' ) ) {
68 64 return array();
69 65 }
70 66
@@ -90,14 +86,12 @@
90 86 * reproduces the Copilot's historical tool names (`desktop-mode/search-posts`
91 87 * → `search_posts`), so progress labels, the system prompt, and the answer
92 88 * schema keep referring to the same names across the abilities migration.
93 89 *
94 - * @since 0.9.4
95 - *
96 90 * @param string $ability_name Fully-namespaced ability name.
97 91 * @return string
98 92 */
99 -function desktop_mode_ai_ability_tool_name( $ability_name ) {
93 +function openstation_ai_ability_tool_name( $ability_name ) {
100 94 $slug = (string) $ability_name;
101 95 $pos = strpos( $slug, '/' );
102 96 if ( false !== $pos ) {
103 97 $slug = substr( $slug, $pos + 1 );
@@ -110,18 +104,61 @@
110 104 return trim( (string) $slug, '_' );
111 105 }
112 106
113 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 +/**
114 153 * Permission callback: any logged-in user who can read the site.
115 154 *
116 155 * Mirrors the read-only search/navigation tools, which were ungated beyond the
117 156 * Copilot's own logged-in requirement.
118 157 *
119 - * @since 0.9.4
120 - *
121 158 * @return bool
122 159 */
123 -function desktop_mode_ai_ability_can_read() {
160 +function openstation_ai_ability_can_read() {
124 161 return is_user_logged_in() && current_user_can( 'read' );
125 162 }
126 163
127 164 /**
@@ -128,14 +165,12 @@
128 165 * A loose object output schema: typed at the top level, permissive on the rest
129 166 * so `WP_Ability::execute()`'s output validation never rejects a valid handler
130 167 * return (the shapes carry optional/nested fields we don't want to freeze).
131 168 *
132 - * @since 0.9.4
133 - *
134 169 * @param array<string,array<string,mixed>> $properties Documented top-level props.
135 170 * @return array<string,mixed>
136 171 */
137 -function desktop_mode_ai_ability_output_schema( array $properties = array() ) {
172 +function openstation_ai_ability_output_schema( array $properties = array() ) {
138 173 return array(
139 174 'type' => 'object',
140 175 'additionalProperties' => true,
141 176 // Keep as a plain (associative) array: WordPress's schema validator
@@ -148,13 +183,11 @@
148 183
149 184 /**
150 185 * Registers every Copilot ability.
151 186 *
152 - * @since 0.9.4
153 - *
154 187 * @return void
155 188 */
156 -function desktop_mode_ai_register_abilities() {
189 +function openstation_ai_register_abilities() {
157 190 if ( ! function_exists( 'wp_register_ability' ) ) {
158 191 return;
159 192 }
160 193
@@ -164,9 +197,9 @@
164 197 'required' => array( 'query', 'offset' ),
165 198 'properties' => array(
166 199 'query' => array(
167 200 'type' => 'string',
168 - '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.',
169 202 ),
170 203 'offset' => array(
171 204 'type' => 'integer',
172 205 'description' => 'Zero-based starting position. Use 0 for the first batch, 10 for the second, and so on.',
@@ -173,27 +206,48 @@
173 206 ),
174 207 ),
175 208 );
176 209
177 - $search_output = desktop_mode_ai_ability_output_schema(
210 + $search_output = openstation_ai_ability_output_schema(
178 211 array(
179 - 'items' => array( 'type' => 'array', 'description' => 'Matching entities with identity, excerpt, and URLs.' ),
180 - 'count' => array( 'type' => 'integer', 'description' => 'Number of items in this batch.' ),
181 - 'total' => array( 'type' => 'integer', 'description' => 'Total matches across all batches.' ),
182 - '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 + ),
183 228 )
184 229 );
185 230
186 231 $readonly_meta = array(
187 - 'annotations' => array( 'readonly' => true, 'idempotent' => true ),
232 + 'annotations' => array(
233 + 'readonly' => true,
234 + 'idempotent' => true,
235 + ),
188 236 'show_in_rest' => true,
189 - 'mcp' => array( 'public' => true, 'type' => 'tool' ),
237 + 'mcp' => array(
238 + 'public' => true,
239 + 'type' => 'tool',
240 + ),
190 241 );
191 242
192 243 // Admin-only abilities are still read-only, but must not be exposed to
193 244 // external agents over MCP.
194 245 $readonly_private_meta = array(
195 - 'annotations' => array( 'readonly' => true, 'idempotent' => true ),
246 + 'annotations' => array(
247 + 'readonly' => true,
248 + 'idempotent' => true,
249 + ),
196 250 'show_in_rest' => true,
197 251 );
198 252
199 253 wp_register_ability(
@@ -199,16 +253,16 @@
199 253 wp_register_ability(
200 254 'desktop-mode/search-posts',
201 255 array(
202 256 'label' => __( 'Search posts', 'desktop-mode' ),
203 - '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.',
204 - '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,
205 259 'input_schema' => $query_offset_input,
206 260 'output_schema' => $search_output,
207 261 'execute_callback' => static function ( $input ) {
208 - return desktop_mode_ai_search_dispatch_tool( 'search_posts', (array) $input );
262 + return openstation_ai_search_dispatch_tool( 'search_posts', (array) $input );
209 263 },
210 - 'permission_callback' => 'desktop_mode_ai_ability_can_read',
264 + 'permission_callback' => 'openstation_ai_ability_can_read',
211 265 'meta' => $readonly_meta,
212 266 )
213 267 );
214 268
@@ -216,15 +270,15 @@
216 270 'desktop-mode/search-pages',
217 271 array(
218 272 'label' => __( 'Search pages', 'desktop-mode' ),
219 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.',
220 - 'category' => DESKTOP_MODE_AI_ABILITY_CATEGORY,
274 + 'category' => OPENSTATION_AI_ABILITY_CATEGORY,
221 275 'input_schema' => $query_offset_input,
222 276 'output_schema' => $search_output,
223 277 'execute_callback' => static function ( $input ) {
224 - return desktop_mode_ai_search_dispatch_tool( 'search_pages', (array) $input );
278 + return openstation_ai_search_dispatch_tool( 'search_pages', (array) $input );
225 279 },
226 - 'permission_callback' => 'desktop_mode_ai_ability_can_read',
280 + 'permission_callback' => 'openstation_ai_ability_can_read',
227 281 'meta' => $readonly_meta,
228 282 )
229 283 );
230 284
@@ -231,16 +285,16 @@
231 285 wp_register_ability(
232 286 'desktop-mode/search-comments',
233 287 array(
234 288 'label' => __( 'Search comments', 'desktop-mode' ),
235 - '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.',
236 - '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,
237 291 'input_schema' => $query_offset_input,
238 292 'output_schema' => $search_output,
239 293 'execute_callback' => static function ( $input ) {
240 - return desktop_mode_ai_search_dispatch_tool( 'search_comments', (array) $input );
294 + return openstation_ai_search_dispatch_tool( 'search_comments', (array) $input );
241 295 },
242 - 'permission_callback' => 'desktop_mode_ai_ability_can_read',
296 + 'permission_callback' => 'openstation_ai_ability_can_read',
243 297 'meta' => $readonly_meta,
244 298 )
245 299 );
246 300
@@ -247,10 +301,10 @@
247 301 wp_register_ability(
248 302 'desktop-mode/search-comments-by-post',
249 303 array(
250 304 'label' => __( 'Search comments on a post', 'desktop-mode' ),
251 - '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.',
252 - '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,
253 307 'input_schema' => array(
254 308 'type' => 'object',
255 309 'additionalProperties' => false,
256 310 'required' => array( 'post_id', 'query', 'offset' ),
@@ -270,11 +324,11 @@
270 324 ),
271 325 ),
272 326 'output_schema' => $search_output,
273 327 'execute_callback' => static function ( $input ) {
274 - 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 );
275 329 },
276 - 'permission_callback' => 'desktop_mode_ai_ability_can_read',
330 + 'permission_callback' => 'openstation_ai_ability_can_read',
277 331 'meta' => $readonly_meta,
278 332 )
279 333 );
280 334
@@ -281,10 +335,16 @@
281 335 wp_register_ability(
282 336 'desktop-mode/list-admin-pages',
283 337 array(
284 338 'label' => __( 'List admin pages', 'desktop-mode' ),
285 - '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. Once you have the catalog, select the 1-3 most relevant entries for the user\'s query and include them in your answer under admin_links with answer_type="navigation". The catalog is small and stable so one call is enough.',
286 - 'category' => DESKTOP_MODE_AI_ABILITY_CATEGORY,
339 + // Describes the TOOL, not the caller's answer format: agents
340 + // and the Copilot consume the same registry, and the
341 + // Copilot's `admin_links` / `answer_type` contract exists in
342 + // its own system prompt and answer schema. Naming those
343 + // fields here would instruct an agent to emit a shape its
344 + // answer schema does not have.
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.',
346 + 'category' => OPENSTATION_AI_ABILITY_CATEGORY,
287 347 'input_schema' => array(
288 348 'type' => 'object',
289 349 'additionalProperties' => false,
290 350 'required' => array(),
@@ -289,15 +349,20 @@
289 349 'additionalProperties' => false,
290 350 'required' => array(),
291 351 'properties' => (object) array(),
292 352 ),
293 - 'output_schema' => desktop_mode_ai_ability_output_schema(
294 - 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 + )
295 360 ),
296 361 'execute_callback' => static function ( $input ) {
297 - return desktop_mode_ai_search_dispatch_tool( 'list_admin_pages', (array) $input );
362 + return openstation_ai_search_dispatch_tool( 'list_admin_pages', (array) $input );
298 363 },
299 - 'permission_callback' => 'desktop_mode_ai_ability_can_read',
364 + 'permission_callback' => 'openstation_ai_ability_can_read',
300 365 'meta' => $readonly_meta,
301 366 )
302 367 );
303 368
@@ -304,10 +369,10 @@
304 369 wp_register_ability(
305 370 'desktop-mode/search-wporg-plugins',
306 371 array(
307 372 'label' => __( 'Search WordPress.org plugins', 'desktop-mode' ),
308 - '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. Present the results as admin_links with answer_type="navigation", titled "Plugin Name · 5M+ installs · 4.8★".',
309 - '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,
310 375 'input_schema' => array(
311 376 'type' => 'object',
312 377 'additionalProperties' => false,
313 378 'required' => array( 'query' ),
@@ -317,18 +382,21 @@
317 382 'description' => 'Plain-language search terms — e.g. "seo", "backup", "caching", "woocommerce", "contact form".',
318 383 ),
319 384 ),
320 385 ),
321 - 'output_schema' => desktop_mode_ai_ability_output_schema(
386 + 'output_schema' => openstation_ai_ability_output_schema(
322 387 array(
323 - '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 + ),
324 392 'count' => array( 'type' => 'integer' ),
325 393 )
326 394 ),
327 395 'execute_callback' => static function ( $input ) {
328 - return desktop_mode_ai_search_dispatch_tool( 'search_wporg_plugins', (array) $input );
396 + return openstation_ai_search_dispatch_tool( 'search_wporg_plugins', (array) $input );
329 397 },
330 - 'permission_callback' => 'desktop_mode_ai_ability_can_read',
398 + 'permission_callback' => 'openstation_ai_ability_can_read',
331 399 'meta' => $readonly_meta,
332 400 )
333 401 );
334 402
@@ -336,9 +404,9 @@
336 404 'desktop-mode/get-php-error-log',
337 405 array(
338 406 'label' => __( 'Read PHP error log', 'desktop-mode' ),
339 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.',
340 - 'category' => DESKTOP_MODE_AI_ABILITY_CATEGORY,
408 + 'category' => OPENSTATION_AI_ABILITY_CATEGORY,
341 409 'input_schema' => array(
342 410 'type' => 'object',
343 411 'additionalProperties' => false,
344 412 'required' => array( 'lines' ),
@@ -348,16 +416,19 @@
348 416 'description' => 'How many recent log lines to return (1-500). Use 20-50 for a quick look, 100-200 for wider context.',
349 417 ),
350 418 ),
351 419 ),
352 - 'output_schema' => desktop_mode_ai_ability_output_schema(
420 + 'output_schema' => openstation_ai_ability_output_schema(
353 421 array(
354 422 'log_available' => array( 'type' => 'boolean' ),
355 - '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 + ),
356 427 )
357 428 ),
358 429 'execute_callback' => static function ( $input ) {
359 - 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 );
360 431 },
361 432 // Admin-only — mirrors the previous in-dispatcher manage_options gate.
362 433 'permission_callback' => static function () {
363 434 return current_user_can( 'manage_options' );
@@ -365,31 +436,32 @@
365 436 'meta' => $readonly_private_meta,
366 437 )
367 438 );
368 439
369 - desktop_mode_ai_register_comment_analysis_ability();
440 + openstation_ai_register_comment_analysis_ability();
370 441 }
371 -add_action( 'wp_abilities_api_init', 'desktop_mode_ai_register_abilities' );
442 +add_action( 'wp_abilities_api_init', 'openstation_ai_register_abilities' );
372 443
373 444 /**
374 445 * Registers the comment-spam analysis ability.
375 446 *
376 447 * Not offered to the model during a search turn (see
377 - * {@see desktop_mode_ai_search_ability_names()}); the moderation pipeline
378 - * resolves and executes it directly ({@see desktop_mode_ai_analyze_comment_now()}
379 - * 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.
380 454 *
381 - * @since 0.9.4
382 - *
383 455 * @return void
384 456 */
385 -function desktop_mode_ai_register_comment_analysis_ability() {
457 +function openstation_ai_register_comment_analysis_ability() {
386 458 wp_register_ability(
387 459 'desktop-mode/analyze-comment',
388 460 array(
389 461 'label' => __( 'Analyze comment for spam', 'desktop-mode' ),
390 - '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.',
391 - '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,
392 464 'input_schema' => array(
393 465 'type' => 'object',
394 466 'additionalProperties' => false,
395 467 'required' => array( 'comment_id' ),
@@ -399,9 +471,9 @@
399 471 'description' => 'The WordPress ID of the comment to analyze.',
400 472 ),
401 473 ),
402 474 ),
403 - 'output_schema' => desktop_mode_ai_ability_output_schema(
475 + 'output_schema' => openstation_ai_ability_output_schema(
404 476 array(
405 477 'topic' => array( 'type' => 'string' ),
406 478 'ai_summary' => array( 'type' => 'string' ),
407 479 'harmful' => array( 'type' => 'boolean' ),
@@ -407,14 +479,17 @@
407 479 'harmful' => array( 'type' => 'boolean' ),
408 480 'spam' => array( 'type' => 'boolean' ),
409 481 )
410 482 ),
411 - 'execute_callback' => 'desktop_mode_ai_ability_analyze_comment',
483 + 'execute_callback' => 'openstation_ai_ability_analyze_comment',
412 484 'permission_callback' => static function () {
413 485 return current_user_can( 'moderate_comments' );
414 486 },
415 487 'meta' => array(
416 - 'annotations' => array( 'readonly' => true, 'idempotent' => true ),
488 + 'annotations' => array(
489 + 'readonly' => true,
490 + 'idempotent' => true,
491 + ),
417 492 'show_in_rest' => true,
418 493 ),
419 494 )
420 495 );
@@ -422,18 +497,16 @@
422 497
423 498 /**
424 499 * Execute callback for the `desktop-mode/analyze-comment` ability.
425 500 *
426 - * @since 0.9.4
427 - *
428 501 * @param array<string,mixed> $input Validated input (`comment_id`).
429 502 * @return array|WP_Error Structured verdict, or an error.
430 503 */
431 -function desktop_mode_ai_ability_analyze_comment( $input ) {
504 +function openstation_ai_ability_analyze_comment( $input ) {
432 505 $comment_id = isset( $input['comment_id'] ) ? (int) $input['comment_id'] : 0;
433 506 $comment = $comment_id > 0 ? get_comment( $comment_id ) : null;
434 507 if ( ! $comment instanceof WP_Comment ) {
435 - 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' ) );
436 509 }
437 510
438 - return desktop_mode_ai_analyze_comment_now( $comment, (int) $comment->user_id );
511 + return openstation_ai_analyze_comment_now( $comment, (int) $comment->user_id );
439 512 }