@diviops/mcp-server 0.2.28 → 0.2.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -5
- package/dist/compatibility.d.ts +1 -1
- package/dist/compatibility.js +1 -1
- package/dist/index.js +271 -2
- package/dist/wp-cli-fs-validator.d.ts +6 -2
- package/dist/wp-cli-fs-validator.js +64 -2
- package/dist/wp-cli.d.ts +19 -3
- package/dist/wp-cli.js +95 -56
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -96,9 +96,9 @@ The server connects via standard WordPress REST API and works with any environme
|
|
|
96
96
|
|
|
97
97
|
> **WP-CLI note:** `WP_PATH` keeps the existing Local by Flywheel behavior by running `wp` directly on the host filesystem. For Docker-based environments (DDEV, wp-env, DevKinsta, WordPress Studio), set `WP_CLI_CMD` to the wrapper command instead. When `WP_CLI_CMD` is set, the server executes the wrapper from `WP_PATH` if provided, otherwise from its current working directory. The MCP server still validates the requested WP-CLI subcommand against its allowlist before executing either path.
|
|
98
98
|
|
|
99
|
-
## Available Tools (
|
|
99
|
+
## Available Tools (63)
|
|
100
100
|
|
|
101
|
-
### Read (
|
|
101
|
+
### Read (30)
|
|
102
102
|
| Tool | Description |
|
|
103
103
|
|------|-------------|
|
|
104
104
|
| `diviops_test_connection` | Test WordPress connection and Divi version |
|
|
@@ -128,8 +128,11 @@ The server connects via standard WordPress REST API and works with any environme
|
|
|
128
128
|
| `diviops_variables_used_on_page` | Detect which `gvid-` (numeric/font) IDs a single page emits — the exact set Divi 5.4.0+ uses to scope selective `:root{--gvid-*}` CSS variable emission. Walks the same content stack the frontend assembles (post_content + active TB header/body/footer + appended canvases + presets). `gcid-` colors are out of scope (separate emission path). Use for per-page orphan validation, preflight before bulk variable rename, or to debug why a numeric/font variable doesn't render on a specific page. Read-only |
|
|
129
129
|
| `diviops_list_canvases` | List all canvas pages |
|
|
130
130
|
| `diviops_get_canvas` | Get canvas content |
|
|
131
|
+
| `diviops_scf_status` | Show SCF (Secure Custom Fields) sync status — pending JSON-vs-DB drift across field groups, post types, taxonomies, options pages. Wraps `wp scf json status` |
|
|
132
|
+
| `diviops_scf_list_field_groups` | List all SCF/ACF field groups (post_name = ACF key, post_title, post_status, post_modified). Queries the `acf-field-group` post type via `wp post list` (works on SCF 6.8.4+ and older ACF) |
|
|
133
|
+
| `diviops_scf_get_field_group` | Fetch a single SCF/ACF field-group post by ACF key (`group_abc123` → post_name) or numeric WP post ID. For the parsed/structured field tree, use `diviops_scf_export --field-groups=<key> --stdout` |
|
|
131
134
|
|
|
132
|
-
### Write (
|
|
135
|
+
### Write (31)
|
|
133
136
|
| Tool | Description |
|
|
134
137
|
|------|-------------|
|
|
135
138
|
| `diviops_create_page` | Create a new page with optional Divi content |
|
|
@@ -160,6 +163,9 @@ The server connects via standard WordPress REST API and works with any environme
|
|
|
160
163
|
| `diviops_create_canvas` | Create a canvas page |
|
|
161
164
|
| `diviops_update_canvas` | Update canvas content |
|
|
162
165
|
| `diviops_delete_canvas` | Delete a canvas page |
|
|
166
|
+
| `diviops_scf_export` | Export SCF schema (field groups, post types, taxonomies, options pages) as JSON to a directory under the safe-root, or to stdout. Wraps `wp scf json export` |
|
|
167
|
+
| `diviops_scf_import` | Import SCF schema from a JSON file (mutates DB; idempotent — existing items are updated). Wraps `wp scf json import <file>` |
|
|
168
|
+
| `diviops_scf_sync` | Apply pending JSON-on-disk SCF changes to the DB. Defaults to `dry_run: true` for safety. Wraps `wp scf json sync` |
|
|
163
169
|
|
|
164
170
|
### Utility (2)
|
|
165
171
|
| Tool | Description |
|
|
@@ -182,7 +188,7 @@ Read-only commands plus non-destructive writes needed for core MCP functionality
|
|
|
182
188
|
| Post meta | `post meta get`, `post meta list`, `post meta set`, `post meta update` |
|
|
183
189
|
| Post types | `post-type list`, `post-type get` |
|
|
184
190
|
| Taxonomies | `taxonomy list`, `term list`, `term create`, `term update` |
|
|
185
|
-
| ACF / SCF | `acf export`, `acf import`, `acf field-group list`, `acf field-group get` |
|
|
191
|
+
| ACF / SCF | `acf export`, `acf import`, `acf field-group list`, `acf field-group get`, `scf json {status,sync,import,export}` (also aliased as `acf json …` per SCF 6.8.4+) |
|
|
186
192
|
| Users | `user list` |
|
|
187
193
|
| Cache | `cache flush`, `transient delete`, `rewrite flush` |
|
|
188
194
|
| Export | `export` (WXR data export to file) |
|
|
@@ -237,12 +243,13 @@ The sentinel grants exactly the extended set above — it does NOT unlock anythi
|
|
|
237
243
|
|
|
238
244
|
### Filesystem flag validation
|
|
239
245
|
|
|
240
|
-
The
|
|
246
|
+
The DEFAULT-tier filesystem commands (`wp export`, `acf export <path>`, `acf import <path>`, `scf json export --dir=<path>`, `scf json import <file>`, plus the `acf json …` aliases) are second-pass validated against a safe root so wrong-path arguments can't write WXR / schema JSON to the web root or read configs from arbitrary locations.
|
|
241
247
|
|
|
242
248
|
- **Safe root**: `<WP_PATH>/.diviops-tmp/` by default (auto-created on first use in host mode). Override with `DIVIOPS_WP_CLI_SAFE_FS_ROOT=/absolute/path`. All path arguments must canonicalize under this directory; symlinks are resolved via `realpath` so a planted symlink inside the safe root pointing outside it is caught.
|
|
243
249
|
- **`wp export` must pass `--dir=<path-under-safe-root>`** (or `--stdout`). Without `--dir`, wp-cli writes to the current working directory; on prod that's typically the web root.
|
|
244
250
|
- **`--filename_format=` must be a filename template**, not a path — separators (`/`, `\`) are rejected so a crafted template can't escape `--dir`'s scope.
|
|
245
251
|
- **`acf export/import`'s positional path** must resolve under the safe root.
|
|
252
|
+
- **`scf json export`'s `--dir=` flag** must resolve under the safe root (or pass `--stdout` for in-memory transfer). **`scf json import`'s positional `<file>` path** must resolve under the safe root.
|
|
246
253
|
- **Wrapper mode (`WP_CLI_CMD`)**: the host-derived safe root doesn't correspond to the wrapper's filesystem (e.g., container paths like `/www/app`), so `DIVIOPS_WP_CLI_SAFE_FS_ROOT` is **required** and must be set to the container-namespace path. FS-sensitive commands are rejected with a clear error if it's missing.
|
|
247
254
|
- **Escape hatch**: `DIVIOPS_WP_CLI_UNSAFE_FS=1` disables validation entirely. Appropriate for trusted single-user local-dev setups that don't want the guard.
|
|
248
255
|
|
package/dist/compatibility.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Version compatibility between MCP server and WP plugin.
|
|
3
3
|
*/
|
|
4
4
|
/** Minimum WP plugin version this server requires. */
|
|
5
|
-
export declare const MIN_PLUGIN_VERSION = "1.0.0-beta.
|
|
5
|
+
export declare const MIN_PLUGIN_VERSION = "1.0.0-beta.44";
|
|
6
6
|
/**
|
|
7
7
|
* Compare two semver-like version strings (supports pre-release tags).
|
|
8
8
|
*
|
package/dist/compatibility.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Version compatibility between MCP server and WP plugin.
|
|
3
3
|
*/
|
|
4
4
|
/** Minimum WP plugin version this server requires. */
|
|
5
|
-
export const MIN_PLUGIN_VERSION = '1.0.0-beta.
|
|
5
|
+
export const MIN_PLUGIN_VERSION = '1.0.0-beta.44';
|
|
6
6
|
/**
|
|
7
7
|
* Compare two semver-like version strings (supports pre-release tags).
|
|
8
8
|
*
|
package/dist/index.js
CHANGED
|
@@ -521,7 +521,7 @@ server.registerTool("diviops_get_section", {
|
|
|
521
521
|
};
|
|
522
522
|
});
|
|
523
523
|
server.registerTool("diviops_update_module", {
|
|
524
|
-
description: 'Update specific attributes of a module. Target by auto_index (e.g. "text:5"), admin label, or text content. Uses dot notation for attribute paths. Example: {"content.decoration.headingFont.h2.font.desktop.value.color": "#ff0000"}. Priority: auto_index > label > match_text. Use occurrence with label when duplicates exist.',
|
|
524
|
+
description: 'Update specific attributes of a module. Target by auto_index (e.g. "text:5"), admin label, or text content. Uses dot notation for attribute paths. Example: {"content.decoration.headingFont.h2.font.desktop.value.color": "#ff0000"}. For paths whose key segments contain literal dots — notably Composable Settings preset slots like groupPreset["title.decoration.spacing"] — escape the inner dots with `\\.` to keep the segment intact: {"groupPreset.title\\\\.decoration\\\\.spacing.presetId": ["uuid"]}. Priority: auto_index > label > match_text. Use occurrence with label when duplicates exist.',
|
|
525
525
|
inputSchema: {
|
|
526
526
|
page_id: z.number().describe("WordPress post/page ID"),
|
|
527
527
|
label: z
|
|
@@ -1322,7 +1322,7 @@ server.registerTool("diviops_delete_canvas", {
|
|
|
1322
1322
|
});
|
|
1323
1323
|
// ── WP-CLI ──────────────────────────────────────────────────────────
|
|
1324
1324
|
server.registerTool("diviops_wp_cli", {
|
|
1325
|
-
description: "Run a WP-CLI command on the WordPress site. Requires WP_PATH env var (LOCAL_SITE_ID auto-detected from Local by Flywheel), or WP_CLI_CMD for containerized wrappers. Commands validated against a safety allowlist. Default tier covers read ops across options/posts/post-types/taxonomies/users/info/core/db, non-destructive writes (post/term create+update, post meta read/write, cache/rewrite/transient flush), ACF schema ops (export/import/list/get
|
|
1325
|
+
description: "Run a WP-CLI command on the WordPress site. Requires WP_PATH env var (LOCAL_SITE_ID auto-detected from Local by Flywheel), or WP_CLI_CMD for containerized wrappers. Commands validated against a safety allowlist. Default tier covers read ops across options/posts/post-types/taxonomies/users/info/core/db, non-destructive writes (post/term create+update, post meta read/write, cache/rewrite/transient flush), ACF/SCF schema ops (`acf export/import/field-group list/get` plus SCF 6.8.4+ `scf json {status,sync,import,export}` and the `acf json …` aliases), and WXR export. Extended tier (requires DIVIOPS_WP_CLI_ALLOW env var) adds destructive or bulk-modifying ops: option update, post/post meta/term delete, search-replace, import, plugin activate/deactivate, eval-file. Filesystem-touching commands (`wp export`, `acf export/import`, `scf|acf json export/import`) are additionally constrained: path arguments must resolve under a safe root (defaults to `<WP_PATH>/.diviops-tmp/`, overridable via DIVIOPS_WP_CLI_SAFE_FS_ROOT, disable via DIVIOPS_WP_CLI_UNSAFE_FS=1); `wp export` and `scf json export` require an explicit `--dir=<path>` (or `--stdout`). In WP_CLI_CMD wrapper mode, DIVIOPS_WP_CLI_SAFE_FS_ROOT is required for FS-sensitive commands. Prefer the typed `diviops_scf_*` wrappers for SCF round-trips — they're easier to invoke and accept the same safe-root scoping. Use --format=json for structured output. Full allowlist + tier rationale + filesystem semantics in the MCP server README.",
|
|
1326
1326
|
inputSchema: {
|
|
1327
1327
|
command: z
|
|
1328
1328
|
.string()
|
|
@@ -1345,6 +1345,275 @@ server.registerTool("diviops_wp_cli", {
|
|
|
1345
1345
|
: `Error: ${result.error}\n${result.output}`;
|
|
1346
1346
|
return { content: [{ type: "text", text: output }] };
|
|
1347
1347
|
});
|
|
1348
|
+
// ── SCF (Secure Custom Fields / ACF) wrappers ───────────────────────
|
|
1349
|
+
//
|
|
1350
|
+
// Typed wrappers over SCF 6.8.4+'s `wp scf json {status,sync,import,export}`
|
|
1351
|
+
// CLI family (also reachable as `wp acf json …`). The plugin file at
|
|
1352
|
+
// wp-content/plugins/secure-custom-fields/src/CLI/JsonCommand.php is the
|
|
1353
|
+
// upstream source of truth for flag shapes — keep these wrappers aligned.
|
|
1354
|
+
function ensureWpCli() {
|
|
1355
|
+
if (!wpCli) {
|
|
1356
|
+
return {
|
|
1357
|
+
ok: false,
|
|
1358
|
+
text: "WP-CLI not configured. Set WP_PATH (Local by Flywheel auto-detect) " +
|
|
1359
|
+
"or WP_CLI_CMD (containerized wrappers) to enable SCF round-trip tools.",
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
return { ok: true };
|
|
1363
|
+
}
|
|
1364
|
+
function pushScfFlag(args, name, value) {
|
|
1365
|
+
if (!value)
|
|
1366
|
+
return;
|
|
1367
|
+
// Each `--name=value` becomes a single argv entry — execFile handles spaces
|
|
1368
|
+
// and quotes inside the value transparently. No string concatenation, no
|
|
1369
|
+
// parseCommand round-trip, so values like "Bob's Group" or filenames with
|
|
1370
|
+
// spaces flow through verbatim.
|
|
1371
|
+
args.push(`--${name}=${value}`);
|
|
1372
|
+
}
|
|
1373
|
+
server.registerTool("diviops_scf_status", {
|
|
1374
|
+
description: "Show SCF (Secure Custom Fields) sync status — how many field groups, post types, taxonomies, and options pages have JSON-on-disk newer than the database (or absent from DB). Read-only. Wraps `wp scf json status`. Requires SCF 6.8.4+ and WP_PATH or WP_CLI_CMD.",
|
|
1375
|
+
inputSchema: {
|
|
1376
|
+
type: z
|
|
1377
|
+
.enum(["field-group", "post-type", "taxonomy", "options-page"])
|
|
1378
|
+
.optional()
|
|
1379
|
+
.describe("Limit to a single item type. Defaults to all types. options-page requires ACF PRO."),
|
|
1380
|
+
detailed: z
|
|
1381
|
+
.boolean()
|
|
1382
|
+
.optional()
|
|
1383
|
+
.describe("List the individual pending items (key/title/type/action) instead of just counts."),
|
|
1384
|
+
},
|
|
1385
|
+
}, async ({ type, detailed }) => {
|
|
1386
|
+
const gate = ensureWpCli();
|
|
1387
|
+
if (!gate.ok) {
|
|
1388
|
+
return { content: [{ type: "text", text: gate.text }] };
|
|
1389
|
+
}
|
|
1390
|
+
const args = ["scf", "json", "status", "--format=json"];
|
|
1391
|
+
pushScfFlag(args, "type", type);
|
|
1392
|
+
if (detailed)
|
|
1393
|
+
args.push("--detailed");
|
|
1394
|
+
const result = await wpCli.runArgs(args);
|
|
1395
|
+
const output = result.success
|
|
1396
|
+
? result.output
|
|
1397
|
+
: `Error: ${result.error}\n${result.output}`;
|
|
1398
|
+
return { content: [{ type: "text", text: output }] };
|
|
1399
|
+
});
|
|
1400
|
+
server.registerTool("diviops_scf_export", {
|
|
1401
|
+
description: "Export SCF field groups, post types, taxonomies, and options pages as JSON — to a directory under the safe-root (`<WP_PATH>/.diviops-tmp/` by default, override via DIVIOPS_WP_CLI_SAFE_FS_ROOT) or to stdout. Wraps `wp scf json export`. Either `dir` or `stdout: true` is required. Filters can be combined; without filters, all items are exported. Note: SCF writes a fixed filename `acf-export-YYYY-MM-DD.json` inside `dir` — two exports on the same day silently overwrite. Copy/rename if you're archiving baselines.",
|
|
1402
|
+
inputSchema: {
|
|
1403
|
+
dir: z
|
|
1404
|
+
.string()
|
|
1405
|
+
.optional()
|
|
1406
|
+
.describe("Absolute output directory under the WP-CLI safe-root. Mutually exclusive with `stdout`. SCF writes a single `acf-export-YYYY-MM-DD.json` file inside this dir."),
|
|
1407
|
+
stdout: z
|
|
1408
|
+
.boolean()
|
|
1409
|
+
.optional()
|
|
1410
|
+
.describe("Print JSON to stdout instead of writing a file. Mutually exclusive with `dir`."),
|
|
1411
|
+
field_groups: z
|
|
1412
|
+
.string()
|
|
1413
|
+
.optional()
|
|
1414
|
+
.describe("Comma-separated field-group ACF keys (`group_abc123`) or admin titles (`My Field Group`). NOT WP post slugs — SCF matches against the def's `key` field or its `title` (case-insensitive). Use `diviops_scf_list_field_groups` to discover keys (post_name column)."),
|
|
1415
|
+
post_types: z
|
|
1416
|
+
.string()
|
|
1417
|
+
.optional()
|
|
1418
|
+
.describe("Comma-separated SCF post-type def keys (`post_type_xxx`) or admin titles (`Programm`). IMPORTANT: this is the SCF def's identifier, NOT the registered post-type slug (`event`, `book`). The registered slug is what `wp post list` and REST URLs use, but SCF's filter matches against the def's `key` field or its `title`. To discover def keys, run `diviops_scf_export --stdout` (no filter) and inspect the top-level entries with `parent='post-type'`."),
|
|
1419
|
+
taxonomies: z
|
|
1420
|
+
.string()
|
|
1421
|
+
.optional()
|
|
1422
|
+
.describe("Comma-separated SCF taxonomy def keys (`taxonomy_xxx`) or admin titles. Same caveat as `post_types`: NOT the registered taxonomy slug — the SCF def's `key` or `title`. Discover via `diviops_scf_export --stdout`."),
|
|
1423
|
+
options_pages: z
|
|
1424
|
+
.string()
|
|
1425
|
+
.optional()
|
|
1426
|
+
.describe("Comma-separated options-page def keys or admin titles. Requires ACF PRO."),
|
|
1427
|
+
},
|
|
1428
|
+
}, async ({ dir, stdout, field_groups, post_types, taxonomies, options_pages }) => {
|
|
1429
|
+
const gate = ensureWpCli();
|
|
1430
|
+
if (!gate.ok) {
|
|
1431
|
+
return { content: [{ type: "text", text: gate.text }] };
|
|
1432
|
+
}
|
|
1433
|
+
if (!dir && !stdout) {
|
|
1434
|
+
return {
|
|
1435
|
+
content: [
|
|
1436
|
+
{
|
|
1437
|
+
type: "text",
|
|
1438
|
+
text: "Error: pass either `dir` (absolute path under DIVIOPS_WP_CLI_SAFE_FS_ROOT) or `stdout: true`.",
|
|
1439
|
+
},
|
|
1440
|
+
],
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
if (dir && stdout) {
|
|
1444
|
+
return {
|
|
1445
|
+
content: [
|
|
1446
|
+
{
|
|
1447
|
+
type: "text",
|
|
1448
|
+
text: "Error: `dir` and `stdout` are mutually exclusive — pick one.",
|
|
1449
|
+
},
|
|
1450
|
+
],
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
const args = ["scf", "json", "export"];
|
|
1454
|
+
if (stdout)
|
|
1455
|
+
args.push("--stdout");
|
|
1456
|
+
pushScfFlag(args, "dir", dir);
|
|
1457
|
+
pushScfFlag(args, "field-groups", field_groups);
|
|
1458
|
+
pushScfFlag(args, "post-types", post_types);
|
|
1459
|
+
pushScfFlag(args, "taxonomies", taxonomies);
|
|
1460
|
+
pushScfFlag(args, "options-pages", options_pages);
|
|
1461
|
+
const result = await wpCli.runArgs(args);
|
|
1462
|
+
const output = result.success
|
|
1463
|
+
? result.output
|
|
1464
|
+
: `Error: ${result.error}\n${result.output}`;
|
|
1465
|
+
return { content: [{ type: "text", text: output }] };
|
|
1466
|
+
});
|
|
1467
|
+
server.registerTool("diviops_scf_import", {
|
|
1468
|
+
description: "Import SCF field groups, post types, taxonomies, options pages from a JSON file. Mutates the database. File path must resolve under the safe-root (`<WP_PATH>/.diviops-tmp/` by default, override via DIVIOPS_WP_CLI_SAFE_FS_ROOT). Idempotent — existing items with matching keys are updated. Wraps `wp scf json import <file>`.",
|
|
1469
|
+
inputSchema: {
|
|
1470
|
+
file: z
|
|
1471
|
+
.string()
|
|
1472
|
+
.describe("Absolute path to the .json file to import. Must resolve under DIVIOPS_WP_CLI_SAFE_FS_ROOT."),
|
|
1473
|
+
},
|
|
1474
|
+
}, async ({ file }) => {
|
|
1475
|
+
const gate = ensureWpCli();
|
|
1476
|
+
if (!gate.ok) {
|
|
1477
|
+
return { content: [{ type: "text", text: gate.text }] };
|
|
1478
|
+
}
|
|
1479
|
+
const result = await wpCli.runArgs(["scf", "json", "import", file]);
|
|
1480
|
+
const output = result.success
|
|
1481
|
+
? result.output
|
|
1482
|
+
: `Error: ${result.error}\n${result.output}`;
|
|
1483
|
+
return { content: [{ type: "text", text: output }] };
|
|
1484
|
+
});
|
|
1485
|
+
server.registerTool("diviops_scf_sync", {
|
|
1486
|
+
description: "Apply pending JSON-on-disk SCF changes to the database. Reads JSON files from the theme/plugin acf-json directory and creates/updates DB entries. Defaults to `dry_run: true` for safety — caller must opt in to mutation. Wraps `wp scf json sync`.",
|
|
1487
|
+
inputSchema: {
|
|
1488
|
+
type: z
|
|
1489
|
+
.enum(["field-group", "post-type", "taxonomy", "options-page"])
|
|
1490
|
+
.optional()
|
|
1491
|
+
.describe("Limit sync to a single item type."),
|
|
1492
|
+
key: z
|
|
1493
|
+
.string()
|
|
1494
|
+
.optional()
|
|
1495
|
+
.describe("Sync only the item with this ACF key (e.g. `group_abc123`)."),
|
|
1496
|
+
dry_run: z
|
|
1497
|
+
.boolean()
|
|
1498
|
+
.optional()
|
|
1499
|
+
.default(true)
|
|
1500
|
+
.describe("Preview pending changes without mutating the database. Defaults to true. Pass `false` to commit."),
|
|
1501
|
+
},
|
|
1502
|
+
}, async ({ type, key, dry_run }) => {
|
|
1503
|
+
const gate = ensureWpCli();
|
|
1504
|
+
if (!gate.ok) {
|
|
1505
|
+
return { content: [{ type: "text", text: gate.text }] };
|
|
1506
|
+
}
|
|
1507
|
+
const args = ["scf", "json", "sync"];
|
|
1508
|
+
pushScfFlag(args, "type", type);
|
|
1509
|
+
pushScfFlag(args, "key", key);
|
|
1510
|
+
if (dry_run !== false)
|
|
1511
|
+
args.push("--dry-run");
|
|
1512
|
+
const result = await wpCli.runArgs(args);
|
|
1513
|
+
const output = result.success
|
|
1514
|
+
? result.output
|
|
1515
|
+
: `Error: ${result.error}\n${result.output}`;
|
|
1516
|
+
return { content: [{ type: "text", text: output }] };
|
|
1517
|
+
});
|
|
1518
|
+
server.registerTool("diviops_scf_list_field_groups", {
|
|
1519
|
+
description: "List all SCF/ACF field groups in the database (post_name = ACF key, post_title, post_status, post_modified). Read-only. Queries the underlying `acf-field-group` post type via `wp post list` — works on both SCF 6.8.4+ (which dropped the legacy `wp acf field-group …` family in favor of the `wp scf json` namespace) and older ACF installs.",
|
|
1520
|
+
}, async () => {
|
|
1521
|
+
const gate = ensureWpCli();
|
|
1522
|
+
if (!gate.ok) {
|
|
1523
|
+
return { content: [{ type: "text", text: gate.text }] };
|
|
1524
|
+
}
|
|
1525
|
+
const result = await wpCli.runArgs([
|
|
1526
|
+
"post",
|
|
1527
|
+
"list",
|
|
1528
|
+
"--post_type=acf-field-group",
|
|
1529
|
+
"--post_status=any",
|
|
1530
|
+
"--fields=ID,post_name,post_title,post_status,post_modified",
|
|
1531
|
+
"--format=json",
|
|
1532
|
+
]);
|
|
1533
|
+
const output = result.success
|
|
1534
|
+
? result.output
|
|
1535
|
+
: `Error: ${result.error}\n${result.output}`;
|
|
1536
|
+
return { content: [{ type: "text", text: output }] };
|
|
1537
|
+
});
|
|
1538
|
+
server.registerTool("diviops_scf_get_field_group", {
|
|
1539
|
+
description: "Fetch a single SCF/ACF field group from the `acf-field-group` post type — by ACF key (`group_abc123`, looked up via `post_name`) or by numeric WP post ID. Returns the WP post fields (post_name, post_title, post_content with serialized fields blob, post_status, post_modified). For the parsed/structured field tree including nested fields, use `diviops_scf_export --field-groups=<key> --stdout` instead. Read-only. SCF 6.8.4 dropped the legacy `wp acf field-group get` command, so this wrapper queries the post type directly via `wp post`.",
|
|
1540
|
+
inputSchema: {
|
|
1541
|
+
key: z
|
|
1542
|
+
.string()
|
|
1543
|
+
.describe("ACF field-group key (`group_abc123`, matched against post_name) or numeric WP post ID."),
|
|
1544
|
+
},
|
|
1545
|
+
}, async ({ key }) => {
|
|
1546
|
+
const gate = ensureWpCli();
|
|
1547
|
+
if (!gate.ok) {
|
|
1548
|
+
return { content: [{ type: "text", text: gate.text }] };
|
|
1549
|
+
}
|
|
1550
|
+
// If the input looks like a numeric ID, hand it to `wp post get` directly.
|
|
1551
|
+
// Otherwise treat it as an ACF key and resolve via post_name first.
|
|
1552
|
+
const isNumericId = /^\d+$/.test(key);
|
|
1553
|
+
if (isNumericId) {
|
|
1554
|
+
const result = await wpCli.runArgs([
|
|
1555
|
+
"post",
|
|
1556
|
+
"get",
|
|
1557
|
+
key,
|
|
1558
|
+
"--format=json",
|
|
1559
|
+
]);
|
|
1560
|
+
const output = result.success
|
|
1561
|
+
? result.output
|
|
1562
|
+
: `Error: ${result.error}\n${result.output}`;
|
|
1563
|
+
return { content: [{ type: "text", text: output }] };
|
|
1564
|
+
}
|
|
1565
|
+
// Resolve ACF key → post ID via `wp post list --name=<key>`. Single-row
|
|
1566
|
+
// lookup; returns [] if the key isn't found.
|
|
1567
|
+
const lookup = await wpCli.runArgs([
|
|
1568
|
+
"post",
|
|
1569
|
+
"list",
|
|
1570
|
+
"--post_type=acf-field-group",
|
|
1571
|
+
"--post_status=any",
|
|
1572
|
+
`--name=${key}`,
|
|
1573
|
+
"--fields=ID",
|
|
1574
|
+
"--format=json",
|
|
1575
|
+
]);
|
|
1576
|
+
if (!lookup.success) {
|
|
1577
|
+
return {
|
|
1578
|
+
content: [
|
|
1579
|
+
{
|
|
1580
|
+
type: "text",
|
|
1581
|
+
text: `Error looking up field-group key "${key}": ${lookup.error}\n${lookup.output}`,
|
|
1582
|
+
},
|
|
1583
|
+
],
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
let postId = null;
|
|
1587
|
+
try {
|
|
1588
|
+
const rows = JSON.parse(lookup.output);
|
|
1589
|
+
if (Array.isArray(rows) && rows.length > 0) {
|
|
1590
|
+
postId = String(rows[0].ID);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
catch {
|
|
1594
|
+
// Fall through — postId stays null, return a clear "not found" error.
|
|
1595
|
+
}
|
|
1596
|
+
if (!postId) {
|
|
1597
|
+
return {
|
|
1598
|
+
content: [
|
|
1599
|
+
{
|
|
1600
|
+
type: "text",
|
|
1601
|
+
text: `No field-group found for key "${key}". Use diviops_scf_list_field_groups to see available keys (post_name field).`,
|
|
1602
|
+
},
|
|
1603
|
+
],
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
const result = await wpCli.runArgs([
|
|
1607
|
+
"post",
|
|
1608
|
+
"get",
|
|
1609
|
+
postId,
|
|
1610
|
+
"--format=json",
|
|
1611
|
+
]);
|
|
1612
|
+
const output = result.success
|
|
1613
|
+
? result.output
|
|
1614
|
+
: `Error: ${result.error}\n${result.output}`;
|
|
1615
|
+
return { content: [{ type: "text", text: output }] };
|
|
1616
|
+
});
|
|
1348
1617
|
// ── Connection ──────────────────────────────────────────────────────
|
|
1349
1618
|
server.registerTool("diviops_test_connection", {
|
|
1350
1619
|
description: "Test the connection to the WordPress site and verify the Divi MCP plugin is active.",
|
|
@@ -53,8 +53,12 @@ export declare function fsValidationDisabled(): boolean;
|
|
|
53
53
|
export declare function ensureSafeFsRoot(safeRoot: string): void;
|
|
54
54
|
/**
|
|
55
55
|
* Identify which FS-sensitive command a parsed arg vector represents.
|
|
56
|
-
* Returns the canonical prefix ("export", "acf export", "
|
|
57
|
-
* null if the args don't match a FS-sensitive command.
|
|
56
|
+
* Returns the canonical prefix ("export", "acf export", "scf json export"…)
|
|
57
|
+
* or null if the args don't match a FS-sensitive command.
|
|
58
|
+
*
|
|
59
|
+
* Checks 3-, 2-, then 1-word prefixes — longer matches win so that
|
|
60
|
+
* `scf json export` is identified as the SCF command (not bare `export`)
|
|
61
|
+
* even though both share the trailing word.
|
|
58
62
|
*/
|
|
59
63
|
export declare function matchFsSensitiveCommand(args: string[]): string | null;
|
|
60
64
|
/**
|
|
@@ -31,11 +31,19 @@ const SAFE_FS_ROOT_OVERRIDE_ENV = 'DIVIOPS_WP_CLI_SAFE_FS_ROOT';
|
|
|
31
31
|
* DEFAULT-tier commands whose arguments can write/read to arbitrary paths.
|
|
32
32
|
* EXTENDED-tier FS commands (`import`, `eval-file`) are opt-in and not
|
|
33
33
|
* validated here — opting in implies accepting the path-scope risk.
|
|
34
|
+
*
|
|
35
|
+
* SCF 6.8.4 introduced `wp scf json export|import` (and `wp acf json …`
|
|
36
|
+
* aliases). `export` takes `--dir=<directory>` (flag, like `wp export`);
|
|
37
|
+
* `import` takes a positional `<file>` path (like the legacy `acf import`).
|
|
34
38
|
*/
|
|
35
39
|
const FS_SENSITIVE_COMMANDS = [
|
|
36
40
|
'export',
|
|
37
41
|
'acf export',
|
|
38
42
|
'acf import',
|
|
43
|
+
'scf json export',
|
|
44
|
+
'scf json import',
|
|
45
|
+
'acf json export',
|
|
46
|
+
'acf json import',
|
|
39
47
|
];
|
|
40
48
|
/**
|
|
41
49
|
* Resolve the effective safe filesystem root for this wp-cli instance.
|
|
@@ -70,12 +78,19 @@ export function ensureSafeFsRoot(safeRoot) {
|
|
|
70
78
|
}
|
|
71
79
|
/**
|
|
72
80
|
* Identify which FS-sensitive command a parsed arg vector represents.
|
|
73
|
-
* Returns the canonical prefix ("export", "acf export", "
|
|
74
|
-
* null if the args don't match a FS-sensitive command.
|
|
81
|
+
* Returns the canonical prefix ("export", "acf export", "scf json export"…)
|
|
82
|
+
* or null if the args don't match a FS-sensitive command.
|
|
83
|
+
*
|
|
84
|
+
* Checks 3-, 2-, then 1-word prefixes — longer matches win so that
|
|
85
|
+
* `scf json export` is identified as the SCF command (not bare `export`)
|
|
86
|
+
* even though both share the trailing word.
|
|
75
87
|
*/
|
|
76
88
|
export function matchFsSensitiveCommand(args) {
|
|
77
89
|
if (args.length === 0)
|
|
78
90
|
return null;
|
|
91
|
+
const threeWord = args.slice(0, 3).join(' ');
|
|
92
|
+
if (FS_SENSITIVE_COMMANDS.includes(threeWord))
|
|
93
|
+
return threeWord;
|
|
79
94
|
const twoWord = args.slice(0, 2).join(' ');
|
|
80
95
|
if (FS_SENSITIVE_COMMANDS.includes(twoWord))
|
|
81
96
|
return twoWord;
|
|
@@ -333,5 +348,52 @@ export function validateFilesystemFlags(args, safeRoot, opts = {}) {
|
|
|
333
348
|
}
|
|
334
349
|
return { allowed: true };
|
|
335
350
|
}
|
|
351
|
+
if (cmd === 'scf json export' || cmd === 'acf json export') {
|
|
352
|
+
// SCF 6.8.4's `wp scf|acf json export` uses `--dir=<dir>` (or `--stdout`)
|
|
353
|
+
// for output destination — same flag shape as `wp export`. Reuse the same
|
|
354
|
+
// extractor; `--filename_format` is `wp export`-only and irrelevant here.
|
|
355
|
+
const { dir, stdout } = extractExportFlags(args);
|
|
356
|
+
if (stdout) {
|
|
357
|
+
// Writes JSON to stdout; no FS write to validate.
|
|
358
|
+
return { allowed: true };
|
|
359
|
+
}
|
|
360
|
+
if (!dir) {
|
|
361
|
+
// Let wp-cli surface its own "must specify --dir or --stdout" error
|
|
362
|
+
// rather than returning a redundant allowlist rejection.
|
|
363
|
+
return { allowed: true };
|
|
364
|
+
}
|
|
365
|
+
const rel = rejectIfRelative(dir, `${cmd} --dir`, safeRootCanonical);
|
|
366
|
+
if (rel)
|
|
367
|
+
return rel;
|
|
368
|
+
if (!isPathUnderSafeRoot(dir, safeRootCanonical)) {
|
|
369
|
+
return {
|
|
370
|
+
allowed: false,
|
|
371
|
+
reason: `${cmd} --dir="${dir}" resolves outside the safe filesystem root ` +
|
|
372
|
+
`"${safeRootCanonical}". Use a path under the safe root, or set ` +
|
|
373
|
+
`${UNSAFE_FS_ENV}=1 to disable validation.`,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
return { allowed: true };
|
|
377
|
+
}
|
|
378
|
+
if (cmd === 'scf json import' || cmd === 'acf json import') {
|
|
379
|
+
// `wp scf|acf json import <file>` — positional file path at args[3]
|
|
380
|
+
// (after the 3-word command prefix). Same safe-root rules as `acf import`.
|
|
381
|
+
const userPath = extractPositionalAfterPrefix(args, 3);
|
|
382
|
+
if (!userPath) {
|
|
383
|
+
return { allowed: true };
|
|
384
|
+
}
|
|
385
|
+
const rel = rejectIfRelative(userPath, cmd, safeRootCanonical);
|
|
386
|
+
if (rel)
|
|
387
|
+
return rel;
|
|
388
|
+
if (!isPathUnderSafeRoot(userPath, safeRootCanonical)) {
|
|
389
|
+
return {
|
|
390
|
+
allowed: false,
|
|
391
|
+
reason: `${cmd} "${userPath}" resolves outside the safe filesystem root ` +
|
|
392
|
+
`"${safeRootCanonical}". Use a path under the safe root, or set ` +
|
|
393
|
+
`${UNSAFE_FS_ENV}=1 to disable validation.`,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
return { allowed: true };
|
|
397
|
+
}
|
|
336
398
|
return { allowed: true };
|
|
337
399
|
}
|
package/dist/wp-cli.d.ts
CHANGED
|
@@ -15,15 +15,31 @@ interface WpCliConfig {
|
|
|
15
15
|
}
|
|
16
16
|
export declare function createWpCli(config: WpCliConfig): {
|
|
17
17
|
/**
|
|
18
|
-
* Execute a WP-CLI command.
|
|
19
|
-
*
|
|
20
|
-
* Uses execFile (no shell) to prevent command injection.
|
|
18
|
+
* Execute a WP-CLI command from a string. Parsed via parseCommand
|
|
19
|
+
* (single/double-quote toggling, no escape support). Validated against
|
|
20
|
+
* the allowlist. Uses execFile (no shell) to prevent command injection.
|
|
21
|
+
*
|
|
22
|
+
* Prefer `runArgs` from typed wrappers — it skips parseCommand entirely
|
|
23
|
+
* so user-supplied values containing apostrophes/quotes flow through
|
|
24
|
+
* verbatim instead of being mis-split.
|
|
21
25
|
*/
|
|
22
26
|
run(command: string): Promise<{
|
|
23
27
|
success: boolean;
|
|
24
28
|
output: string;
|
|
25
29
|
error?: string;
|
|
26
30
|
}>;
|
|
31
|
+
/**
|
|
32
|
+
* Execute a WP-CLI command from a pre-built argv array. Skips
|
|
33
|
+
* parseCommand so values containing whitespace, apostrophes, or
|
|
34
|
+
* quotes pass through unmodified. Same allowlist + FS-safe-root
|
|
35
|
+
* validation as `run`. Use this from typed wrappers that already
|
|
36
|
+
* have the args structured (no string concatenation needed).
|
|
37
|
+
*/
|
|
38
|
+
runArgs(args: string[]): Promise<{
|
|
39
|
+
success: boolean;
|
|
40
|
+
output: string;
|
|
41
|
+
error?: string;
|
|
42
|
+
}>;
|
|
27
43
|
/** Return the list of allowed commands and available extensions. */
|
|
28
44
|
getAllowedCommands(): {
|
|
29
45
|
allowed: string[];
|
package/dist/wp-cli.js
CHANGED
|
@@ -40,6 +40,20 @@ const DEFAULT_COMMANDS = [
|
|
|
40
40
|
'acf import',
|
|
41
41
|
'acf field-group list',
|
|
42
42
|
'acf field-group get',
|
|
43
|
+
// SCF 6.8.4+ adds `wp scf json {status,sync,import,export}` (also aliased as
|
|
44
|
+
// `wp acf json …`). All four are dev-time schema ops — `status`/`sync` are
|
|
45
|
+
// diff/apply against on-disk JSON, `import` re-creates DB entries from JSON,
|
|
46
|
+
// `export` writes DB schema to JSON. Same tier semantics as the legacy `acf`
|
|
47
|
+
// entries above; FS-touching subcommands (export, import) are second-pass
|
|
48
|
+
// validated below.
|
|
49
|
+
'scf json status',
|
|
50
|
+
'scf json sync',
|
|
51
|
+
'scf json import',
|
|
52
|
+
'scf json export',
|
|
53
|
+
'acf json status',
|
|
54
|
+
'acf json sync',
|
|
55
|
+
'acf json import',
|
|
56
|
+
'acf json export',
|
|
43
57
|
// Users (read-only)
|
|
44
58
|
'user list',
|
|
45
59
|
// Cache (non-destructive maintenance)
|
|
@@ -319,66 +333,91 @@ export function createWpCli(config) {
|
|
|
319
333
|
const runOptions = customWpCliCmd
|
|
320
334
|
? { ...execOptions, env, cwd: config.wpPath }
|
|
321
335
|
: { ...execOptions, env };
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
336
|
+
// Internal argv-based runner — used by both `run(string)` (after
|
|
337
|
+
// parseCommand) and `runArgs(string[])` (skip parseCommand). Typed
|
|
338
|
+
// wrappers should call runArgs to bypass parseCommand's quote-toggling
|
|
339
|
+
// weakness: when a value contains an apostrophe (e.g. label "Bob's
|
|
340
|
+
// Group", file path /tmp/it's-fine.json), parseCommand mis-splits the
|
|
341
|
+
// argv because it treats the embedded `'` as a quote toggle. Passing
|
|
342
|
+
// pre-built argv eliminates the parsing step entirely so user-provided
|
|
343
|
+
// strings flow through verbatim — execFile (no shell) handles them
|
|
344
|
+
// correctly. Raised in PR #473 review (Copilot/Gemini both flagged).
|
|
345
|
+
const runArgv = async (args) => {
|
|
346
|
+
const check = isCommandAllowed(args);
|
|
347
|
+
if (!check.allowed) {
|
|
348
|
+
return { success: false, output: '', error: check.reason };
|
|
349
|
+
}
|
|
350
|
+
// Second-pass FS validation for commands whose flags/args can read/write
|
|
351
|
+
// arbitrary paths. Scoped to DEFAULT-tier FS commands only — EXTENDED
|
|
352
|
+
// (`import`, `eval-file`) are opt-in via DIVIOPS_WP_CLI_ALLOW, so opting
|
|
353
|
+
// in signals the caller accepts path-scope risk. Skip entirely when the
|
|
354
|
+
// user explicitly disables via DIVIOPS_WP_CLI_UNSAFE_FS=1.
|
|
355
|
+
//
|
|
356
|
+
// Wrapper mode (WP_CLI_CMD set) is gated separately inside
|
|
357
|
+
// validateFilesystemFlags — host-derived safe roots don't correspond to
|
|
358
|
+
// the wrapper's filesystem namespace, so the validator requires an
|
|
359
|
+
// explicit DIVIOPS_WP_CLI_SAFE_FS_ROOT there. We also skip the host-side
|
|
360
|
+
// mkdir in wrapper mode since the safe root is either container-scoped
|
|
361
|
+
// (user-managed) or unset (validator rejects).
|
|
362
|
+
if (!fsValidationDisabled() && matchFsSensitiveCommand(args)) {
|
|
363
|
+
const safeRoot = resolveSafeFsRoot(config.wpPath);
|
|
364
|
+
if (!customWpCliCmd) {
|
|
365
|
+
ensureSafeFsRoot(safeRoot);
|
|
333
366
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
367
|
+
const fsCheck = validateFilesystemFlags(args, safeRoot, {
|
|
368
|
+
isWrapper: !!customWpCliCmd,
|
|
369
|
+
});
|
|
370
|
+
if (!fsCheck.allowed) {
|
|
371
|
+
return { success: false, output: '', error: fsCheck.reason };
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const fullArgs = customWpCliCmd
|
|
375
|
+
? [...prefixArgs, ...args, '--no-color']
|
|
376
|
+
: [...args, `--path=${config.wpPath}`, '--no-color'];
|
|
377
|
+
return new Promise((resolve) => {
|
|
378
|
+
execFile(executable, fullArgs, runOptions, (error, stdout, stderr) => {
|
|
379
|
+
// Filter PHP deprecation warnings from output
|
|
380
|
+
const output = (stdout + '\n' + stderr)
|
|
381
|
+
.split('\n')
|
|
382
|
+
.filter((line) => !line.includes('Deprecated:') && !line.includes('PHP Deprecated'))
|
|
383
|
+
.join('\n')
|
|
384
|
+
.trim();
|
|
385
|
+
if (error) {
|
|
386
|
+
const detail = error.killed
|
|
387
|
+
? 'Command timed out'
|
|
388
|
+
: error.signal
|
|
389
|
+
? `Killed by signal ${error.signal}`
|
|
390
|
+
: `Exit code ${error.code ?? 'unknown'}`;
|
|
391
|
+
resolve({ success: false, output, error: detail });
|
|
350
392
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
});
|
|
354
|
-
if (!fsCheck.allowed) {
|
|
355
|
-
return { success: false, output: '', error: fsCheck.reason };
|
|
393
|
+
else {
|
|
394
|
+
resolve({ success: true, output });
|
|
356
395
|
}
|
|
357
|
-
}
|
|
358
|
-
const fullArgs = customWpCliCmd
|
|
359
|
-
? [...prefixArgs, ...args, '--no-color']
|
|
360
|
-
: [...args, `--path=${config.wpPath}`, '--no-color'];
|
|
361
|
-
return new Promise((resolve) => {
|
|
362
|
-
execFile(executable, fullArgs, runOptions, (error, stdout, stderr) => {
|
|
363
|
-
// Filter PHP deprecation warnings from output
|
|
364
|
-
const output = (stdout + '\n' + stderr)
|
|
365
|
-
.split('\n')
|
|
366
|
-
.filter((line) => !line.includes('Deprecated:') && !line.includes('PHP Deprecated'))
|
|
367
|
-
.join('\n')
|
|
368
|
-
.trim();
|
|
369
|
-
if (error) {
|
|
370
|
-
const detail = error.killed
|
|
371
|
-
? 'Command timed out'
|
|
372
|
-
: error.signal
|
|
373
|
-
? `Killed by signal ${error.signal}`
|
|
374
|
-
: `Exit code ${error.code ?? 'unknown'}`;
|
|
375
|
-
resolve({ success: false, output, error: detail });
|
|
376
|
-
}
|
|
377
|
-
else {
|
|
378
|
-
resolve({ success: true, output });
|
|
379
|
-
}
|
|
380
|
-
});
|
|
381
396
|
});
|
|
397
|
+
});
|
|
398
|
+
};
|
|
399
|
+
return {
|
|
400
|
+
/**
|
|
401
|
+
* Execute a WP-CLI command from a string. Parsed via parseCommand
|
|
402
|
+
* (single/double-quote toggling, no escape support). Validated against
|
|
403
|
+
* the allowlist. Uses execFile (no shell) to prevent command injection.
|
|
404
|
+
*
|
|
405
|
+
* Prefer `runArgs` from typed wrappers — it skips parseCommand entirely
|
|
406
|
+
* so user-supplied values containing apostrophes/quotes flow through
|
|
407
|
+
* verbatim instead of being mis-split.
|
|
408
|
+
*/
|
|
409
|
+
async run(command) {
|
|
410
|
+
return runArgv(parseCommand(command));
|
|
411
|
+
},
|
|
412
|
+
/**
|
|
413
|
+
* Execute a WP-CLI command from a pre-built argv array. Skips
|
|
414
|
+
* parseCommand so values containing whitespace, apostrophes, or
|
|
415
|
+
* quotes pass through unmodified. Same allowlist + FS-safe-root
|
|
416
|
+
* validation as `run`. Use this from typed wrappers that already
|
|
417
|
+
* have the args structured (no string concatenation needed).
|
|
418
|
+
*/
|
|
419
|
+
async runArgs(args) {
|
|
420
|
+
return runArgv(args);
|
|
382
421
|
},
|
|
383
422
|
/** Return the list of allowed commands and available extensions. */
|
|
384
423
|
getAllowedCommands() {
|