PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.4
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.4
1.3.4 1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 All 30 releases
← All changes | includes/modules/Mcp/Cli_Bridge.php +68 -0 1.1.41.3.4 View file →
@@ -56,8 +56,16 @@
56 56 }
57 57 $map[ $name ] = array(
58 58 'callback' => $callback,
59 59 'shortdesc' => isset( $cmd['shortdesc'] ) ? (string) $cmd['shortdesc'] : '',
60 + // Optional AI-facing description. `shortdesc` is CLI help
61 + // text, written for someone who has ALREADY decided to run
62 + // the command — "Show GZIP status (server type, active,
63 + // mode)". A model reading tools/list has not decided yet
64 + // and needs the opposite: when to reach for this, and what
65 + // question it answers. Same string cannot serve both, and
66 + // rewriting shortdesc would degrade `--help`. (#184)
67 + 'ai_hint' => isset( $cmd['ai_hint'] ) ? (string) $cmd['ai_hint'] : '',
60 68 'synopsis' => isset( $cmd['synopsis'] ) && is_array( $cmd['synopsis'] ) ? $cmd['synopsis'] : array(),
61 69 'module' => $module->slug(),
62 70 );
63 71 }
@@ -154,8 +162,68 @@
154 162 if ( '' !== $error ) {
155 163 $result['error'] = $error;
156 164 }
157 165 return $result;
166 + }
167 +
168 + /**
169 + * Resolve any (command, args) pair to the canonical command name plus
170 + * its leading action, using the SAME resolution `run()` performs.
171 + *
172 + * Callers reach one action by many spellings — `("db", ["clean"])`,
173 + * `("database clean")`, `("xspeed db", ["clean", "--types=x"])` — and a
174 + * guard that compares raw strings only stops the spelling it was written
175 + * against. Anything deciding whether a call is destructive must classify
176 + * it here, not parse the caller's input itself.
177 + *
178 + * @param string $command Raw command string, any accepted spelling.
179 + * @param string[] $args Positional args, if any.
180 + * @return array{name:string,action:string} name is '' when unresolved.
181 + */
182 + public static function classify( string $command, array $args = array() ): array {
183 + $input = self::normalize( $command );
184 + if ( '' === $input ) {
185 + return array(
186 + 'name' => '',
187 + 'action' => '',
188 + );
189 + }
190 +
191 + list( $name, $extra ) = self::resolve( $input, self::commands() );
192 +
193 + /*
194 + * Fall back to a structural split when the registry cannot resolve the
195 + * input — an unbooted module, a command that is not registered on this
196 + * install, or a bare unit-test context all leave commands() empty.
197 + *
198 + * Callers that ask "is this destructive?" must never be told "no"
199 + * merely because the registry was unavailable: that turns a missing
200 + * module into a disarmed confirmation. Splitting "xspeed db clean"
201 + * into name "xspeed db" + action "clean" costs nothing when the
202 + * command does not exist (run() rejects it as unknown a moment later)
203 + * and keeps the guard closed when it does.
204 + */
205 + if ( '' === $name ) {
206 + $parts = explode( ' ', $input );
207 + $name = implode( ' ', array_slice( $parts, 0, 2 ) );
208 + $extra = array_slice( $parts, 2 );
209 + }
210 +
211 + // Trailing words from the command string come before explicit args —
212 + // identical to run(), so the action seen here is the action that runs.
213 + $merged = array_merge( $extra, array_values( $args ) );
214 + $action = '';
215 + foreach ( $merged as $candidate ) {
216 + if ( is_scalar( $candidate ) && '' !== trim( (string) $candidate ) ) {
217 + $action = strtolower( trim( (string) $candidate ) );
218 + break;
219 + }
220 + }
221 +
222 + return array(
223 + 'name' => $name,
224 + 'action' => $action,
225 + );
158 226 }
159 227
160 228 /**
161 229 * Normalize a command name: trim, collapse whitespace, and ensure the