PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / src / Agent / workflows / abilities / tools / execute-ability.js

execute-ability.js in Extendify 3.2.1, at src/Agent/workflows/abilities/tools/execute-ability.js

84 lines 2.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import apiFetch from '@wordpress/api-fetch';
2 import { __, sprintf } from '@wordpress/i18n';
3 import { addQueryArgs } from '@wordpress/url';
4
5 // Core's run controller 405s any verb that doesn't match these annotations.
6 const verbFor = (annotations) => {
7 if (annotations?.readonly) return 'GET';
8 if (annotations?.destructive && annotations?.idempotent) return 'DELETE';
9 return 'POST';
10 };
11
12 // Without these, a bare run of an all-optional ability 400s — core validates
13 // `input` against the schema and ignores property-level defaults.
14 const schemaDefaults = (schema) =>
15 Object.fromEntries(
16 Object.entries(schema?.properties ?? {})
17 .filter(([, property]) => property?.default != null)
18 .map(([key, property]) => [key, property.default]),
19 );
20
21 // Query strings stringify values; PHP treats "false" as truthy, "0" as falsy.
22 const encodeBooleans = (value) => {
23 if (typeof value === 'boolean') return value ? 1 : 0;
24 if (Array.isArray(value)) return value.map(encodeBooleans);
25 if (value && typeof value === 'object')
26 return Object.fromEntries(
27 Object.entries(value).map(([k, v]) => [k, encodeBooleans(v)]),
28 );
29 return value;
30 };
31
32 export default async ({ ability, input }) => {
33 const descriptor = (window.extAgentData?.wpAbilities ?? [])
34 .flatMap((category) => category.abilities ?? [])
35 .find((a) => a.name === ability);
36 if (!descriptor?.runHref) {
37 // translators: %s is the machine name of a WordPress ability the agent tried to run.
38 throw new Error(
39 sprintf(__('Ability "%s" is not available.', 'extendify-local'), ability),
40 );
41 }
42
43 // Optional inputs the model didn't fill come through as null at any depth —
44 // drop them so schema defaults stand in and the ability sees only real values.
45 const isEmptyEntry = (value) =>
46 !!value &&
47 typeof value === 'object' &&
48 !Array.isArray(value) &&
49 !Object.keys(value).length;
50 const withoutNulls = (value) => {
51 // An entry the model left blank still has to satisfy the item schema.
52 if (Array.isArray(value))
53 return value.map(withoutNulls).filter((entry) => !isEmptyEntry(entry));
54 if (value && typeof value === 'object') {
55 return Object.fromEntries(
56 Object.entries(value)
57 .filter(([, v]) => v != null)
58 .map(([k, v]) => [k, withoutNulls(v)]),
59 );
60 }
61 return value;
62 };
63 const filled = withoutNulls(input ?? {});
64 const provided = { ...schemaDefaults(descriptor.inputSchema), ...filled };
65 const hasInput = Object.keys(provided).length > 0;
66
67 const method = verbFor(descriptor.annotations);
68 // The run controller reads `input` from the JSON body for POST, but from the
69 // query string for GET/DELETE — pass it where WP will actually look.
70 if (method === 'POST') {
71 return await apiFetch({
72 url: descriptor.runHref,
73 method,
74 ...(hasInput && { data: { input: provided } }),
75 });
76 }
77 return await apiFetch({
78 url: hasInput
79 ? addQueryArgs(descriptor.runHref, { input: encodeBooleans(provided) })
80 : descriptor.runHref,
81 method,
82 });
83 };
84