@prenta/mcp-server 0.23.0

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.
@@ -0,0 +1,1348 @@
1
+ /**
2
+ * Thin HTTP client for the Prenta CMS REST API. We deliberately don't depend
3
+ * on @prenta/core here — that would force the MCP server to bring
4
+ * in Prisma, all the server-side modules, and a Next.js runtime context. The
5
+ * MCP server only needs to translate MCP tool calls into HTTP requests.
6
+ */
7
+ export interface PrentaClientOptions {
8
+ /** Base URL of the CMS, e.g. `https://maidpro.co`. The /api/cms path is appended automatically. */
9
+ baseUrl: string;
10
+ /** API key (`act_sk_...`). */
11
+ apiKey: string;
12
+ /** Optional fetch implementation override (used in tests). */
13
+ fetchImpl?: typeof fetch;
14
+ }
15
+ /**
16
+ * Thrown by `PrentaClient` on non-2xx responses. Carries the HTTP
17
+ * `status` so callers (e.g. `list_collections`'s OpenAPI fallback) can
18
+ * branch on the status code rather than pattern-matching the error
19
+ * message — pattern matching was rejected by review on PR #155 because
20
+ * a real 404 might omit the literal substring "not found" and an
21
+ * unrelated 5xx response might include it.
22
+ */
23
+ export declare class PrentaClientError extends Error {
24
+ readonly status: number;
25
+ constructor(message: string, status: number);
26
+ }
27
+ export declare class PrentaClient {
28
+ readonly baseUrl: string;
29
+ readonly apiKey: string;
30
+ private readonly fetchImpl;
31
+ constructor(opts: PrentaClientOptions);
32
+ private headers;
33
+ private request;
34
+ /**
35
+ * POST a `multipart/form-data` body. Distinct from `request()` because:
36
+ *
37
+ * - we MUST NOT set `Content-Type` (the fetch implementation appends the
38
+ * multipart boundary; if we override it the server can't parse the body),
39
+ * - we MUST NOT `JSON.stringify(form)` — a `FormData` is sent verbatim,
40
+ * - the upload handler in `cms-core` rejects requests without a valid
41
+ * `Content-Length` header, and Node 20+ / undici populate that header
42
+ * automatically when the body is a `FormData`.
43
+ *
44
+ * Used exclusively by `uploadMedia` today. Kept as a thin private helper so
45
+ * the failure mode (multipart parse error vs JSON parse error vs HTTP
46
+ * error) stays observable to the caller.
47
+ */
48
+ private requestMultipart;
49
+ listDocuments(collection: string, query?: {
50
+ page?: number;
51
+ pageSize?: number;
52
+ status?: string;
53
+ search?: string;
54
+ slug?: string;
55
+ }): Promise<{
56
+ data: {
57
+ docs: any[];
58
+ total: number;
59
+ page: number;
60
+ pageSize: number;
61
+ };
62
+ }>;
63
+ getDocument(collection: string, id: string): Promise<{
64
+ data: any;
65
+ }>;
66
+ createDocument(collection: string, data: Record<string, unknown>): Promise<{
67
+ data: any;
68
+ }>;
69
+ updateDocument(collection: string, id: string, data: Record<string, unknown>): Promise<{
70
+ data: any;
71
+ }>;
72
+ deleteDocument(collection: string, id: string): Promise<{
73
+ data: {
74
+ deleted: boolean;
75
+ };
76
+ }>;
77
+ getGlobal(slug: string): Promise<{
78
+ data: any;
79
+ }>;
80
+ updateGlobal(slug: string, data: Record<string, unknown>): Promise<{
81
+ data: any;
82
+ }>;
83
+ /**
84
+ * List media assets. The `GET /media` endpoint returns the asset array under
85
+ * `items`/`data` (and pagination fields), never `docs`. We normalize it into
86
+ * the same `{ data: { docs, total, page, pageSize, totalPages } }` envelope
87
+ * as `listDocuments` so the `list_media` MCP tool output is consistent with
88
+ * `list_documents` and an agent can read `docs` uniformly.
89
+ */
90
+ listMedia(query?: {
91
+ page?: number;
92
+ pageSize?: number;
93
+ }): Promise<{
94
+ data: {
95
+ docs: any[];
96
+ total: number;
97
+ page: number;
98
+ pageSize: number;
99
+ totalPages: number;
100
+ };
101
+ }>;
102
+ /**
103
+ * Upload a binary asset to the CMS media library. Bytes are accepted as a
104
+ * `Uint8Array` / `Buffer` (the MCP tool wrapper handles base64 decoding
105
+ * from the JSON-RPC layer, but the client surface stays binary).
106
+ *
107
+ * The server enforces a 50 MB hard cap and validates magic bytes against
108
+ * the declared `mimeType` — uploading a `.exe` with `image/png` will be
109
+ * rejected with HTTP 415. SVGs are sanitised server-side. Pass
110
+ * `skipOptimize: true` to keep the original bytes; by default JPEG / PNG
111
+ * are recompressed to WebP.
112
+ */
113
+ uploadMedia(input: {
114
+ filename: string;
115
+ mimeType: string;
116
+ bytes: Uint8Array | Buffer;
117
+ skipOptimize?: boolean;
118
+ /**
119
+ * When true, skip content-hash dedupe and always create a new record
120
+ * even if identical bytes already exist (#718).
121
+ */
122
+ forceUpload?: boolean;
123
+ }): Promise<{
124
+ data: any;
125
+ }>;
126
+ /**
127
+ * Update media metadata. Only the supplied fields are touched. `alt` maps
128
+ * to the DB column `altText`; `focalX` / `focalY` (range 0..1) drive
129
+ * smart-crop on the front end. `caption`, `description`, and `focusKeyword`
130
+ * round out the fields `calculateMediaSeoScore` reads, so an agent can take
131
+ * an asset all the way to a passing media SEO grade.
132
+ */
133
+ updateMedia(id: string, body: {
134
+ alt?: string;
135
+ title?: string;
136
+ filename?: string;
137
+ caption?: string;
138
+ description?: string;
139
+ focusKeyword?: string;
140
+ focalX?: number;
141
+ focalY?: number;
142
+ /** WCAG decorative image — empty alt is intentional (#714). */
143
+ isDecorative?: boolean;
144
+ }): Promise<{
145
+ data: any;
146
+ }>;
147
+ /**
148
+ * Vision-backed alt text for a media library image. Requires the AI plugin,
149
+ * a vision-capable provider, and `ai.features.mediaAltText` enabled.
150
+ * Returns a suggestion — call `update_media({ alt })` to publish it.
151
+ */
152
+ generateMediaAlt(body: {
153
+ mediaId?: string;
154
+ imageUrl?: string;
155
+ pageTitle?: string;
156
+ surroundingText?: string;
157
+ purpose?: 'decorative' | 'informative' | 'functional';
158
+ }): Promise<{
159
+ data: {
160
+ alt: string;
161
+ title: string;
162
+ alternatives: string[];
163
+ isDecorative: boolean;
164
+ confidence: number;
165
+ usedVision: boolean;
166
+ };
167
+ }>;
168
+ /** Delete a media item AND its underlying blob storage object (best-effort). */
169
+ deleteMedia(id: string): Promise<{
170
+ data: {
171
+ success: boolean;
172
+ };
173
+ }>;
174
+ /**
175
+ * Bulk-update media metadata. Per-item patches (not a shared patch). Returns
176
+ * per-id success/failure so a large backfill can land partial results (#717).
177
+ */
178
+ bulkUpdateMedia(items: Array<{
179
+ id: string;
180
+ alt?: string;
181
+ title?: string;
182
+ filename?: string;
183
+ caption?: string;
184
+ description?: string;
185
+ focusKeyword?: string;
186
+ focalX?: number;
187
+ focalY?: number;
188
+ isDecorative?: boolean;
189
+ }>): Promise<{
190
+ data: {
191
+ updated: number;
192
+ failed: number;
193
+ results: Array<{
194
+ id: string;
195
+ ok: true;
196
+ data: any;
197
+ } | {
198
+ id: string;
199
+ ok: false;
200
+ error: string;
201
+ status?: number;
202
+ }>;
203
+ };
204
+ }>;
205
+ /**
206
+ * Bulk-delete media items and their blobs (best-effort). Requires
207
+ * `confirm: true` at the MCP tool layer; the API also requires it.
208
+ */
209
+ bulkDeleteMedia(ids: string[], confirm: true): Promise<{
210
+ data: {
211
+ deleted: number;
212
+ failed: number;
213
+ results: Array<{
214
+ id: string;
215
+ ok: true;
216
+ } | {
217
+ id: string;
218
+ ok: false;
219
+ error: string;
220
+ status?: number;
221
+ }>;
222
+ };
223
+ }>;
224
+ /**
225
+ * Recompress an existing image to WebP. Returns the updated media row plus
226
+ * an `optimization` summary (`{ originalSize, optimizedSize, savings, ... }`).
227
+ * Already-WebP files and non-images short-circuit with `alreadyOptimized: true`.
228
+ */
229
+ optimizeMedia(id: string): Promise<{
230
+ data: any;
231
+ }>;
232
+ /**
233
+ * Move a media item into a folder, or out of every folder by passing
234
+ * `folderId: null`. The server validates that the target folder lives in
235
+ * the `media` scope.
236
+ */
237
+ moveMediaToFolder(id: string, folderId: string | null): Promise<{
238
+ data: {
239
+ success: boolean;
240
+ };
241
+ }>;
242
+ /**
243
+ * List folders in a given scope as a nested tree (`{ ...folder, children: [...] }`).
244
+ * Scopes today: `'media'` for the media library, `'documents'` / `'collections'`
245
+ * for document folders. The `scope` query param is required by the server.
246
+ */
247
+ listFolders(scope: string): Promise<{
248
+ data: any[];
249
+ }>;
250
+ /**
251
+ * Create a folder. If `parentId` is supplied, it must already exist AND live
252
+ * in the same scope — the server refuses cross-scope reparenting.
253
+ */
254
+ createFolder(body: {
255
+ name: string;
256
+ scope: string;
257
+ parentId?: string | null;
258
+ }): Promise<{
259
+ data: any;
260
+ }>;
261
+ /**
262
+ * Update a folder: rename (`name`), reparent (`parentId`; pass `null` to
263
+ * promote to root), or reorder (`position`). The server prevents self-
264
+ * parenting and cross-scope reparenting.
265
+ */
266
+ updateFolder(id: string, body: {
267
+ name?: string;
268
+ parentId?: string | null;
269
+ position?: number;
270
+ }): Promise<{
271
+ data: any;
272
+ }>;
273
+ /**
274
+ * Delete a folder. Refuses with HTTP 409 if the folder still contains
275
+ * documents, media, or sub-folders — the caller must move or delete the
276
+ * contents first.
277
+ */
278
+ deleteFolder(id: string): Promise<{
279
+ data: {
280
+ success: boolean;
281
+ };
282
+ }>;
283
+ /**
284
+ * Move a document into a folder, or remove it from every folder by passing
285
+ * `folderId: null`. Server validates that the target folder is in the
286
+ * documents / collections scope.
287
+ */
288
+ moveDocumentToFolder(id: string, folderId: string | null): Promise<{
289
+ data: {
290
+ success: boolean;
291
+ };
292
+ }>;
293
+ /**
294
+ * List immutable snapshots of a document, newest first. Each `Version`
295
+ * carries the full `data` payload at the time of write plus the user who
296
+ * made the change (joined via `relationLoadStrategy: 'join'` server-side).
297
+ */
298
+ listVersions(collection: string, documentId: string, query?: {
299
+ page?: number;
300
+ pageSize?: number;
301
+ }): Promise<{
302
+ data: {
303
+ versions: any[];
304
+ total: number;
305
+ page: number;
306
+ pageSize: number;
307
+ totalPages: number;
308
+ };
309
+ }>;
310
+ /**
311
+ * Roll a document back to a prior version. The restore goes through the
312
+ * normal `updateDocument` path so it: (a) runs validation hooks, (b)
313
+ * creates a NEW version row capturing the restore (i.e. it never rewrites
314
+ * history), and (c) emits a `version_restored` event to `prenta_audit_logs`.
315
+ */
316
+ restoreVersion(collection: string, documentId: string, versionId: string): Promise<{
317
+ data: any;
318
+ }>;
319
+ /**
320
+ * Schedule a document to publish and/or unpublish at a future timestamp.
321
+ * `scheduledAt` (publish) forces `status` → `SCHEDULED` so the cron picks
322
+ * it up; `scheduledUnpublishAt` leaves the current status alone. The
323
+ * server requires at least one of the two, both must be ISO 8601 strings
324
+ * AND in the future, and `scheduledUnpublishAt` must be after
325
+ * `scheduledAt` when both are provided.
326
+ */
327
+ schedulePublish(collection: string, documentId: string, body: {
328
+ scheduledAt?: string | null;
329
+ scheduledUnpublishAt?: string | null;
330
+ }): Promise<{
331
+ data: {
332
+ id: string;
333
+ status: string;
334
+ scheduledAt: string | null;
335
+ scheduledUnpublishAt: string | null;
336
+ };
337
+ }>;
338
+ /**
339
+ * Cancel any pending publish + unpublish schedule for the document. If
340
+ * the document was in `SCHEDULED` (not yet published) the server drops
341
+ * its status back to `DRAFT`. Already-published documents stay live.
342
+ */
343
+ unschedulePublish(collection: string, documentId: string): Promise<{
344
+ data: {
345
+ id: string;
346
+ status: string;
347
+ scheduledAt: null;
348
+ scheduledUnpublishAt: null;
349
+ };
350
+ }>;
351
+ /**
352
+ * Read the workflow state of a document. Returns the current stage
353
+ * (`DRAFT` | `IN_REVIEW` | `APPROVED` | `PUBLISHED` | `ARCHIVED`), the
354
+ * stages the *current* user is allowed to transition to next (derived
355
+ * from their role), plus the reviewer ID + note if set.
356
+ */
357
+ getWorkflowState(collection: string, documentId: string): Promise<{
358
+ data: {
359
+ stage: string;
360
+ transitions: string[];
361
+ reviewerId: string | null;
362
+ reviewNote: string | null;
363
+ };
364
+ }>;
365
+ /**
366
+ * Move a document to a new workflow stage. The server validates that the
367
+ * transition is allowed from the current stage AND that the caller's
368
+ * role permits it; if either check fails it returns HTTP 400 with the
369
+ * reason in the error body.
370
+ */
371
+ transitionWorkflow(collection: string, documentId: string, body: {
372
+ stage: string;
373
+ note?: string;
374
+ }): Promise<{
375
+ data: {
376
+ success: boolean;
377
+ };
378
+ }>;
379
+ /**
380
+ * List the comment thread attached to a document. By default returns only
381
+ * unresolved, non-deleted comments. `includeResolved` widens that to
382
+ * include resolved threads; `includeDeleted` is admin-only and surfaces
383
+ * soft-deleted moderation history (returns HTTP 403 for non-admin keys).
384
+ */
385
+ listComments(documentId: string, query?: {
386
+ includeResolved?: boolean;
387
+ includeDeleted?: boolean;
388
+ pageSize?: number;
389
+ }): Promise<{
390
+ data: any[];
391
+ }>;
392
+ /**
393
+ * Create a comment on a document. `anchor` is optional and lets the
394
+ * caller pin the comment to a specific text range (used by the
395
+ * Prosemirror selection UI in the admin). `parentId` is set for thread
396
+ * replies. The server emits a `comment_created` event to
397
+ * `prenta_audit_logs` and fires a fire-and-forget notification dispatch.
398
+ */
399
+ createComment(documentId: string, body: {
400
+ body: string;
401
+ anchor?: {
402
+ from: string;
403
+ to: string;
404
+ } | null;
405
+ parentId?: string | null;
406
+ }): Promise<{
407
+ data: any;
408
+ }>;
409
+ /**
410
+ * Run a deterministic accessibility audit over a page-builder tree.
411
+ * Detects missing alt text, heading-level skips, missing form labels,
412
+ * missing ARIA attributes, and undersized touch targets. Returns an
413
+ * array of `A11yIssue` entries — each carries `autoFixable: boolean`
414
+ * so the caller can pair this with `fixPageA11y` for one-click cleanup.
415
+ *
416
+ * Pass the tree exactly as returned by a document's `data.tree` field;
417
+ * no normalisation needed.
418
+ */
419
+ auditPageA11y(tree: unknown): Promise<{
420
+ data: {
421
+ issues: Array<{
422
+ id: string;
423
+ blockId: string;
424
+ type: "missing-alt" | "heading-skip" | "missing-label" | "missing-aria" | "small-touch-target";
425
+ severity: "error" | "warning";
426
+ message: string;
427
+ autoFixable: boolean;
428
+ }>;
429
+ };
430
+ }>;
431
+ /**
432
+ * Auto-fix the accessibility issues marked `autoFixable: true` from
433
+ * `audit_page_a11y` and return the mutated tree plus a report
434
+ * (`fixedCount` / `remainingCount`). Non-auto-fixable issues are
435
+ * returned in `report.issues` for human follow-up. The handler emits a
436
+ * `settings_changed` audit event with the fix counts.
437
+ */
438
+ fixPageA11y(tree: unknown): Promise<{
439
+ data: {
440
+ tree: unknown;
441
+ report: {
442
+ issues: Array<{
443
+ id: string;
444
+ blockId: string;
445
+ type: string;
446
+ severity: "error" | "warning";
447
+ message: string;
448
+ autoFixable: boolean;
449
+ }>;
450
+ fixedCount: number;
451
+ remainingCount: number;
452
+ };
453
+ };
454
+ }>;
455
+ /**
456
+ * Edit a comment body. Only the author can edit; the server returns a
457
+ * `forbidden` error otherwise. Resolved threads remain editable so a
458
+ * moderator can append context.
459
+ */
460
+ editComment(id: string, body: string): Promise<{
461
+ data: any;
462
+ }>;
463
+ /** Mark a comment thread resolved. Requires write-role; emits `comment_resolved` audit event. */
464
+ resolveComment(id: string): Promise<{
465
+ data: any;
466
+ }>;
467
+ /** Re-open a previously resolved comment thread. Requires write-role. */
468
+ reopenComment(id: string): Promise<{
469
+ data: any;
470
+ }>;
471
+ /**
472
+ * Soft-delete a comment. Server returns `forbidden` unless the caller is
473
+ * the author OR an admin. Soft-deleted comments are admin-only viewable
474
+ * via `list_comments({ includeDeleted: true })`.
475
+ */
476
+ deleteComment(id: string): Promise<{
477
+ data: any;
478
+ }>;
479
+ /** List all redirects, newest first. */
480
+ listRedirects(): Promise<{
481
+ data: any[];
482
+ }>;
483
+ /**
484
+ * Create a redirect. Admin-only. The server enforces an open-redirect
485
+ * defence — absolute destinations must point at a host that's either
486
+ * the configured `NEXT_PUBLIC_SITE_URL` or listed in
487
+ * `redirects.allowedExternalHosts`. Relative destinations (`/foo`) are
488
+ * always allowed. `statusCode` accepts 301 / 302 / 307 / 308 only and
489
+ * defaults to 301 on the server.
490
+ */
491
+ createRedirect(body: {
492
+ source: string;
493
+ destination: string;
494
+ statusCode?: 301 | 302 | 307 | 308;
495
+ isRegex?: boolean;
496
+ notes?: string | null;
497
+ }): Promise<{
498
+ data: any;
499
+ }>;
500
+ /** Delete a redirect by ID. Admin-only. */
501
+ deleteRedirect(id: string): Promise<{
502
+ data: {
503
+ success: boolean;
504
+ };
505
+ }>;
506
+ /**
507
+ * List page templates. Admin-only. Optional `category` filter narrows
508
+ * to a single template category. `collection` reorders the result so
509
+ * the suggested templates for that collection bubble to the top
510
+ * (driven by collection presets) and annotates each row with
511
+ * `suggested` / `suggestionRank`. When `collection` is supplied the
512
+ * response also carries a top-level `suggestion` summary.
513
+ */
514
+ listPageTemplates(query?: {
515
+ category?: string;
516
+ collection?: string;
517
+ }): Promise<{
518
+ data: any[];
519
+ suggestion?: any;
520
+ }>;
521
+ /** Fetch a single page template by ID. Admin-only. */
522
+ getPageTemplate(id: string): Promise<{
523
+ data: any;
524
+ }>;
525
+ /** List saved page-builder sections. Admin-only. Optional `category` filter. */
526
+ listSavedSections(query?: {
527
+ category?: string;
528
+ }): Promise<{
529
+ data: any[];
530
+ }>;
531
+ /**
532
+ * Run a site-wide SEO scan over every published document. **Expensive**
533
+ * — touches every published row. Returns
534
+ * `{ total, pagesWithIssues, totalProblems, issues[] }` where each
535
+ * issue carries `{ documentId, title, slug, problems[] }`. The audit
536
+ * recommended gating this behind an explicit confirmation arg; the
537
+ * tool wrapper enforces that, the client is intentionally a thin
538
+ * pass-through.
539
+ */
540
+ seoScanRun(): Promise<{
541
+ data: any;
542
+ }>;
543
+ /**
544
+ * Aggregate SEO health for the dashboard — counts of missing meta
545
+ * descriptions / alt text plus a `topContent` array of recent
546
+ * published docs with light SEO scores. Cheap; safe to call on
547
+ * dashboard refresh.
548
+ */
549
+ seoSummary(): Promise<{
550
+ data: any;
551
+ }>;
552
+ /**
553
+ * Aggregated Overview-tab payload: site score + grade + delta, four
554
+ * headline metrics, open-issue summary, priority-ranked quick wins, and
555
+ * (gated) search-performance rows. Cheap read; derived from the last
556
+ * completed audit run.
557
+ */
558
+ seoOverview(): Promise<{
559
+ data: any;
560
+ }>;
561
+ /**
562
+ * Kick off a persistent SEO audit run. `scope` controls breadth:
563
+ * `full` (everything), `content`, `technical`, or `redirect`. Returns the
564
+ * created `SEOAuditRun` (status, issue counts, before/after site score).
565
+ * Heavier than `seoSummary` — persists issues for the Audit tab.
566
+ */
567
+ runSeoAudit(scope?: 'full' | 'content' | 'technical' | 'redirect'): Promise<{
568
+ data: any;
569
+ }>;
570
+ /** List persisted SEO audit runs, newest first. */
571
+ listSeoAuditRuns(): Promise<{
572
+ data: {
573
+ runs: any[];
574
+ };
575
+ }>;
576
+ /**
577
+ * List SEO issues with optional filters. `severity` (critical/warning/info),
578
+ * `category` (content/technical/redirects/...), `status`
579
+ * (open/ignored/resolved/recurring), and `auditRunId` narrow the result.
580
+ */
581
+ listSeoIssues(query?: {
582
+ severity?: string;
583
+ category?: string;
584
+ status?: string;
585
+ auditRunId?: string;
586
+ }): Promise<{
587
+ data: {
588
+ issues: any[];
589
+ };
590
+ }>;
591
+ /** Fetch a single SEO issue by ID. */
592
+ getSeoIssue(id: string): Promise<{
593
+ data: any;
594
+ }>;
595
+ /** Mark an SEO issue resolved / ignored / reopened. */
596
+ setSeoIssueStatus(id: string, action: 'resolve' | 'ignore' | 'reopen'): Promise<{
597
+ data: any;
598
+ }>;
599
+ /**
600
+ * Export a markdown SEO fix brief for dev-team handoff. Embeds cached AI
601
+ * content briefs and topic research when available.
602
+ */
603
+ exportSeoFixBrief(query?: {
604
+ severity?: string;
605
+ status?: string;
606
+ auditRunId?: string;
607
+ manualOnly?: boolean;
608
+ includeContentBriefs?: boolean;
609
+ includeContentResearch?: boolean;
610
+ }): Promise<{
611
+ data: {
612
+ markdown: string;
613
+ filename: string;
614
+ issueCount: number;
615
+ };
616
+ }>;
617
+ /** Preview a deterministic or AI-assisted inline metadata/schema fix. */
618
+ suggestSeoIssueFix(id: string): Promise<{
619
+ data: {
620
+ suggestion: any;
621
+ };
622
+ }>;
623
+ /** Apply a previously suggested inline fix (requires matching fingerprint). */
624
+ applySeoIssueFix(id: string, fingerprint: string): Promise<{
625
+ data: any;
626
+ }>;
627
+ /** Load a cached AI content brief for a content/heading issue. */
628
+ getSeoIssueContentBrief(id: string): Promise<{
629
+ data: {
630
+ supported: boolean;
631
+ brief: any;
632
+ stale: boolean;
633
+ };
634
+ }>;
635
+ /** Generate (or regenerate) an AI content brief for manual content fixes. */
636
+ generateSeoIssueContentBrief(id: string): Promise<{
637
+ data: {
638
+ brief: any;
639
+ };
640
+ }>;
641
+ /**
642
+ * Run topic research (thin-content) then generate a content brief in one step.
643
+ * Optional competitor URLs are fetched SSRF-safe (max 3).
644
+ */
645
+ researchAndBriefSeoIssue(id: string, competitorUrls?: string[]): Promise<{
646
+ data: {
647
+ research: any;
648
+ brief: any;
649
+ };
650
+ }>;
651
+ /** Batch-generate content briefs for open issues (skips fresh cached briefs). */
652
+ batchGenerateSeoContentBriefs(body?: {
653
+ severity?: string;
654
+ status?: string;
655
+ issueIds?: string[];
656
+ skipExisting?: boolean;
657
+ includeResearch?: boolean;
658
+ competitorUrls?: string[];
659
+ }): Promise<{
660
+ data: {
661
+ generated: number;
662
+ skipped: number;
663
+ failed: number;
664
+ results: Array<{
665
+ issueId: string;
666
+ status: string;
667
+ error?: string;
668
+ }>;
669
+ };
670
+ }>;
671
+ /** List redirect rules with hit counts + stale flags. */
672
+ listRedirectRules(): Promise<{
673
+ data: {
674
+ rules: any[];
675
+ };
676
+ }>;
677
+ /** Redirect overview counts (active / stale / 404s / chains). */
678
+ redirectOverview(): Promise<{
679
+ data: any;
680
+ }>;
681
+ /** Detected redirect chains and loops with safe-flatten suggestions. */
682
+ redirectChains(): Promise<{
683
+ data: {
684
+ chains: any[];
685
+ };
686
+ }>;
687
+ /** Flatten a redirect chain to a single hop (updates the first rule's destination). */
688
+ flattenRedirectChain(chainId: string): Promise<{
689
+ data: any;
690
+ }>;
691
+ /** AI-generated 404 recovery suggestions awaiting review. */
692
+ listRedirectSuggestions(): Promise<{
693
+ data: {
694
+ suggestions: any[];
695
+ };
696
+ }>;
697
+ getRedirectRecoverySettings(): Promise<{
698
+ data: any;
699
+ }>;
700
+ updateRedirectRecoverySettings(body: {
701
+ aiRerank?: boolean;
702
+ autoApply?: boolean;
703
+ autoApplyMinConfidence?: number;
704
+ }): Promise<{
705
+ data: any;
706
+ }>;
707
+ /** Scan tracked 404s and generate recovery redirect suggestions. */
708
+ generateRedirectRecoverySuggestions(): Promise<{
709
+ data: any;
710
+ }>;
711
+ acceptRedirectSuggestion(id: string): Promise<{
712
+ data: any;
713
+ }>;
714
+ dismissRedirectSuggestion(id: string): Promise<{
715
+ data: any;
716
+ }>;
717
+ exportRedirectsCsv(): Promise<{
718
+ data: {
719
+ csv: string;
720
+ };
721
+ }>;
722
+ importRedirectsCsv(csv: string): Promise<{
723
+ data: {
724
+ imported: number;
725
+ errors: string[];
726
+ };
727
+ }>;
728
+ updateRedirectRule(id: string, body: Record<string, unknown>): Promise<{
729
+ data: any;
730
+ }>;
731
+ /** Site-wide internal/external link graph snapshot (Links tab). */
732
+ seoLinkSnapshot(): Promise<{
733
+ data: any;
734
+ }>;
735
+ /** Broken / suspicious outbound link scan (bounded batch). */
736
+ seoLinkHealth(): Promise<{
737
+ data: any;
738
+ }>;
739
+ seoLinkSuggest(body: {
740
+ targetEntityId: string;
741
+ targetEntityType: 'page' | 'post';
742
+ limit?: number;
743
+ }): Promise<{
744
+ data: any;
745
+ }>;
746
+ /** Build or refresh the AI link index used for contextual suggestions. */
747
+ seoLinkAiIndex(): Promise<{
748
+ data: any;
749
+ }>;
750
+ getCrawlSettings(): Promise<{
751
+ data: any;
752
+ }>;
753
+ updateCrawlSettings(body: Record<string, unknown>): Promise<{
754
+ data: any;
755
+ }>;
756
+ getRobotsTxt(): Promise<{
757
+ data: any;
758
+ }>;
759
+ updateRobotsTxt(body: string): Promise<{
760
+ data: any;
761
+ }>;
762
+ getSitemapStatus(): Promise<{
763
+ data: any;
764
+ }>;
765
+ regenerateSitemap(): Promise<{
766
+ data: any;
767
+ }>;
768
+ listSeoContentRecords(): Promise<{
769
+ data: {
770
+ records: any[];
771
+ };
772
+ }>;
773
+ getSeoContentRecord(entityType: string, entityId: string): Promise<{
774
+ data: any;
775
+ }>;
776
+ updateSeoContentFields(entityType: string, entityId: string, body: Record<string, unknown>): Promise<{
777
+ data: any;
778
+ }>;
779
+ aiSeoGenerateField(body: {
780
+ field: string;
781
+ entityType: string;
782
+ entityId: string;
783
+ }): Promise<{
784
+ data: any;
785
+ }>;
786
+ aiSeoExplainIssue(issueId: string): Promise<{
787
+ data: any;
788
+ }>;
789
+ aiSeoGenerateSchema(body: {
790
+ entityType: string;
791
+ entityId: string;
792
+ schemaType: string;
793
+ }): Promise<{
794
+ data: any;
795
+ }>;
796
+ aiSeoSummarize(): Promise<{
797
+ data: any;
798
+ }>;
799
+ /**
800
+ * List submissions for a form. **PII-sensitive** — form submissions
801
+ * typically contain names, emails, free-form messages, and any other
802
+ * fields the form collects. Requires write-role on the API key AND
803
+ * an explicit `formId` (no enumeration). Paged.
804
+ */
805
+ listFormSubmissions(formId: string, query?: {
806
+ page?: number;
807
+ pageSize?: number;
808
+ }): Promise<{
809
+ data: {
810
+ submissions: any[];
811
+ total: number;
812
+ page: number;
813
+ pageSize: number;
814
+ totalPages: number;
815
+ };
816
+ }>;
817
+ /**
818
+ * List entries from the submissions inbox (decrypted PII, attribution,
819
+ * read/star/archive state). `formId` is required at the tool boundary to
820
+ * prevent accidental cross-form enumeration. Returns
821
+ * `{ data: { entries, total, page, pageSize } }`.
822
+ */
823
+ listFormEntries(query: {
824
+ formId: string;
825
+ page?: number;
826
+ pageSize?: number;
827
+ unread?: boolean;
828
+ starred?: boolean;
829
+ archived?: boolean;
830
+ spamStatus?: string;
831
+ }): Promise<{
832
+ data: {
833
+ entries: any[];
834
+ total: number;
835
+ page: number;
836
+ pageSize: number;
837
+ };
838
+ }>;
839
+ /**
840
+ * Read a single entry plus the schema version it was captured against.
841
+ * Returns `{ data: { entry, fields } }`. The server audits this read as
842
+ * `form.entry.viewed` because it exposes decrypted PII.
843
+ */
844
+ getFormEntry(entryId: string): Promise<{
845
+ data: {
846
+ entry: any;
847
+ fields: any[];
848
+ };
849
+ }>;
850
+ /**
851
+ * List form definitions (NOT submissions). Returns
852
+ * `{ data: Form[], total, page, pageSize }` where each form carries its
853
+ * field schema plus the legacy `fieldCount` / `submissions` aliases. Use
854
+ * this to discover form IDs before reading entries or editing a schema.
855
+ */
856
+ listForms(query?: {
857
+ page?: number;
858
+ pageSize?: number;
859
+ status?: string;
860
+ search?: string;
861
+ sortBy?: string;
862
+ sortDirection?: 'asc' | 'desc';
863
+ }): Promise<{
864
+ data: any[];
865
+ total: number;
866
+ page: number;
867
+ pageSize: number;
868
+ }>;
869
+ /** Fetch a single form definition by ID (metadata + field schema). */
870
+ getForm(id: string): Promise<{
871
+ data: any;
872
+ }>;
873
+ /**
874
+ * Create a form. Only `name` is required server-side; the slug is derived
875
+ * from `name` when omitted. `fields` is the initial field schema (see
876
+ * `saveFormSchema` for the field shape) — omit to start with an empty
877
+ * form and add the schema in a second call.
878
+ */
879
+ createForm(body: {
880
+ name: string;
881
+ slug?: string;
882
+ description?: string;
883
+ status?: 'active' | 'draft' | 'archived';
884
+ fields?: Array<Record<string, unknown>>;
885
+ successMessage?: string;
886
+ redirectUrl?: string;
887
+ }): Promise<{
888
+ data: any;
889
+ }>;
890
+ /**
891
+ * Update a form's metadata. Only the supplied fields change. Note the
892
+ * field SCHEMA is NOT editable here — use `saveFormSchema` so each schema
893
+ * change is versioned.
894
+ */
895
+ updateForm(id: string, body: {
896
+ name?: string;
897
+ description?: string;
898
+ status?: 'active' | 'draft' | 'archived';
899
+ successMessage?: string;
900
+ redirectUrl?: string;
901
+ }): Promise<{
902
+ data: any;
903
+ }>;
904
+ /**
905
+ * Delete a form. By default the server refuses (HTTP 409) when the form
906
+ * has submissions; pass `force: true` to delete it and its entries.
907
+ */
908
+ deleteForm(id: string, force?: boolean): Promise<{
909
+ data: {
910
+ success: boolean;
911
+ };
912
+ }>;
913
+ /** Duplicate a form (new draft copy with a fresh slug). Returns the new form. */
914
+ duplicateForm(id: string): Promise<{
915
+ data: any;
916
+ }>;
917
+ /**
918
+ * Read a form's field schema plus its version history:
919
+ * `{ fields, activeVersionId, versions }`. Call before `saveFormSchema`
920
+ * to avoid clobbering the live schema.
921
+ */
922
+ getFormSchema(id: string): Promise<{
923
+ data: {
924
+ fields: any[];
925
+ activeVersionId: string | null;
926
+ versions: any[];
927
+ };
928
+ }>;
929
+ /**
930
+ * Replace a form's field schema. Creates a NEW schema version (history is
931
+ * never rewritten) and points the form at it. `fields` is the full
932
+ * ordered field list — partial patches are not supported (read with
933
+ * `getFormSchema`, mutate, write back the whole array).
934
+ */
935
+ saveFormSchema(id: string, fields: Array<Record<string, unknown>>, notes?: string): Promise<{
936
+ data: {
937
+ form: any;
938
+ version: any;
939
+ };
940
+ }>;
941
+ /**
942
+ * Update inbox state on a single submission entry (mark read/unread,
943
+ * star, archive). Does not touch the captured field values.
944
+ */
945
+ updateFormEntry(entryId: string, patch: {
946
+ unread?: boolean;
947
+ starred?: boolean;
948
+ archived?: boolean;
949
+ }): Promise<{
950
+ data: any;
951
+ }>;
952
+ /** Apply the same inbox-state patch to many entries at once. Returns `{ updated }`. */
953
+ bulkUpdateFormEntries(ids: string[], patch: {
954
+ unread?: boolean;
955
+ starred?: boolean;
956
+ archived?: boolean;
957
+ }): Promise<{
958
+ data: {
959
+ updated: number;
960
+ };
961
+ }>;
962
+ /** Permanently delete a submission entry (and its decrypted PII). Destructive. */
963
+ deleteFormEntry(entryId: string): Promise<{
964
+ data: {
965
+ success: boolean;
966
+ };
967
+ }>;
968
+ /**
969
+ * Export every submission for a form as CSV. The endpoint returns a raw
970
+ * `text/csv` body (NOT a JSON envelope), so we read it as text rather
971
+ * than via `request()`. **Bulk PII egress** — the server records a
972
+ * `form.entries.exported` audit event. Returns the CSV text plus the
973
+ * server-suggested filename.
974
+ */
975
+ exportFormEntries(formId: string): Promise<{
976
+ filename: string;
977
+ csv: string;
978
+ }>;
979
+ /**
980
+ * Read the deployment health of the CMS. Unauthenticated on the server
981
+ * side (still safe to call — exposes only operational status, no
982
+ * tenant data). Returns `{ status, version, env, models,
983
+ * databaseConnected, secretConfigured }`.
984
+ *
985
+ * Recommended as an agent's first call: a `degraded` / `unhealthy`
986
+ * status surfaces operator-level config problems early instead of
987
+ * surfacing as cryptic errors deep in a workflow.
988
+ */
989
+ healthCheck(): Promise<{
990
+ data: any;
991
+ }>;
992
+ generatePage(input: {
993
+ prompt: string;
994
+ template?: string;
995
+ context?: string;
996
+ steps?: string[];
997
+ tone?: string;
998
+ }): Promise<{
999
+ data: {
1000
+ tree: any;
1001
+ steps: any[];
1002
+ totalTokensUsed: number;
1003
+ totalDurationMs: number;
1004
+ };
1005
+ }>;
1006
+ generateBlock(input: {
1007
+ blockType: string;
1008
+ variant?: string;
1009
+ pageContext?: unknown;
1010
+ tone?: string;
1011
+ }): Promise<{
1012
+ data: any;
1013
+ }>;
1014
+ /** One-shot: generate AI page AND persist as a draft (or published) document. */
1015
+ createPageFromPrompt(input: {
1016
+ prompt: string;
1017
+ template?: string;
1018
+ context?: string;
1019
+ steps?: string[];
1020
+ tone?: string;
1021
+ collection?: string;
1022
+ title?: string;
1023
+ slug?: string;
1024
+ status?: 'DRAFT' | 'PUBLISHED';
1025
+ publish?: boolean;
1026
+ }): Promise<{
1027
+ data: {
1028
+ document: any;
1029
+ generation: {
1030
+ steps: any[];
1031
+ totalTokensUsed: number;
1032
+ totalDurationMs: number;
1033
+ };
1034
+ };
1035
+ }>;
1036
+ /**
1037
+ * Generic AI content authoring for any non-page collection. Used by the
1038
+ * per-type MCP tools (create_blog_post, create_case_study, etc.) and by the
1039
+ * generic create_in_collection tool.
1040
+ */
1041
+ createDocumentFromPrompt(collection: string, input: {
1042
+ prompt: string;
1043
+ context?: string;
1044
+ tone?: string;
1045
+ targetAudience?: string;
1046
+ title?: string;
1047
+ slug?: string;
1048
+ status?: 'DRAFT' | 'PUBLISHED';
1049
+ publish?: boolean;
1050
+ }): Promise<{
1051
+ data: {
1052
+ document: any;
1053
+ generation: {
1054
+ usage: {
1055
+ promptTokens: number;
1056
+ completionTokens: number;
1057
+ };
1058
+ durationMs: number;
1059
+ seo: {
1060
+ metaTitle?: string;
1061
+ metaDescription?: string;
1062
+ schemaType?: string;
1063
+ };
1064
+ };
1065
+ };
1066
+ }>;
1067
+ /**
1068
+ * Run the composite content audit (SEO + readability + a11y + freshness)
1069
+ * against a stored document or ad-hoc content. Deterministic — no LLM call,
1070
+ * safe to invoke on every save.
1071
+ */
1072
+ auditDocument(input: {
1073
+ collection?: string;
1074
+ id?: string;
1075
+ title?: string;
1076
+ body?: string;
1077
+ metaTitle?: string;
1078
+ metaDescription?: string;
1079
+ targetKeyword?: string;
1080
+ url?: string;
1081
+ headings?: string[];
1082
+ images?: Array<{
1083
+ src: string;
1084
+ alt?: string;
1085
+ }>;
1086
+ internalLinks?: string[];
1087
+ externalLinks?: string[];
1088
+ publishedAt?: string;
1089
+ updatedAt?: string;
1090
+ contentType?: string;
1091
+ }): Promise<{
1092
+ data: any;
1093
+ }>;
1094
+ /**
1095
+ * Inline AI co-author. Wraps the plugin-ai writing assistant
1096
+ * (rewrite/expand/compress/proofread) behind a single HTTP surface.
1097
+ */
1098
+ coauthor(input: {
1099
+ action: 'rewrite' | 'expand' | 'compress' | 'proofread';
1100
+ text: string;
1101
+ style?: string;
1102
+ tone?: string;
1103
+ instructions?: string;
1104
+ targetLength?: number;
1105
+ collectionSlug?: string;
1106
+ targetAudience?: string;
1107
+ }): Promise<{
1108
+ data: {
1109
+ action: string;
1110
+ text: string;
1111
+ changes?: unknown[];
1112
+ };
1113
+ }>;
1114
+ /**
1115
+ * Suggest internal links from a body of text against the published CMS
1116
+ * documents the API key can read. Optionally scope to a single collection.
1117
+ */
1118
+ suggestInternalLinks(input: {
1119
+ content: string;
1120
+ collection?: string;
1121
+ limit?: number;
1122
+ excludeId?: string;
1123
+ }): Promise<{
1124
+ data: {
1125
+ suggestions: any[];
1126
+ candidatesScanned: number;
1127
+ };
1128
+ }>;
1129
+ /** Fetch the auto-generated OpenAPI spec, useful for tools listing collections + fields. */
1130
+ getOpenApi(): Promise<any>;
1131
+ /**
1132
+ * List configured collections via the dedicated `/collections`
1133
+ * discovery endpoint. Returns `{ data: CollectionMeta[] }` with one
1134
+ * entry per configured collection (`slug`, `labels`, `type`, `fields`,
1135
+ * `fieldCount`, `hidden`). Sourced directly from `getPrentaConfig()`
1136
+ * server-side, so it stays accurate even when OpenAPI spec generation
1137
+ * is missing routes. Newer endpoint — older CMS servers may return
1138
+ * 404; `list_collections` falls back to OpenAPI in that case.
1139
+ */
1140
+ listCollections(): Promise<{
1141
+ data: Array<{
1142
+ slug: string;
1143
+ labels: {
1144
+ singular: string;
1145
+ plural: string;
1146
+ };
1147
+ type: string;
1148
+ urlPrefix: string;
1149
+ hidden: boolean;
1150
+ fieldCount: number;
1151
+ fields: Array<{
1152
+ name: string;
1153
+ type: string;
1154
+ required: boolean;
1155
+ relationTo?: string | string[];
1156
+ hasMany?: boolean;
1157
+ options?: Array<{
1158
+ label: string;
1159
+ value: string;
1160
+ }>;
1161
+ }>;
1162
+ }>;
1163
+ }>;
1164
+ /**
1165
+ * Lightweight user directory lookup used by the comment mention
1166
+ * picker. Returns up to 10 active users whose name OR email matches
1167
+ * `query` (case-insensitive `contains`). Empty / whitespace
1168
+ * queries return an empty array — the endpoint deliberately won't
1169
+ * dump the whole user table.
1170
+ *
1171
+ * Exposed via the `search_users` MCP tool so AI agents can resolve
1172
+ * `@<userId>` mentions when drafting comments.
1173
+ */
1174
+ searchUsers(query: string): Promise<{
1175
+ data: Array<{
1176
+ id: string;
1177
+ name: string;
1178
+ email: string;
1179
+ }>;
1180
+ }>;
1181
+ /** Resolve a public URL path to its document. */
1182
+ resolveUrl(path: string): Promise<{
1183
+ data: any;
1184
+ }>;
1185
+ /**
1186
+ * Soft-check navigation item URLs against published documents + redirects.
1187
+ * Returns warnings only — never blocks save. Requires cms-core with
1188
+ * `POST /navigations/validate-targets`.
1189
+ */
1190
+ validateNavigationTargets(items: unknown[]): Promise<{
1191
+ data: {
1192
+ warnings: string[];
1193
+ unresolved: Array<{
1194
+ itemPath: string;
1195
+ label: string;
1196
+ url: string;
1197
+ normalizedPath: string;
1198
+ }>;
1199
+ };
1200
+ }>;
1201
+ /** Run the deterministic SEO content analysis against a stored document. */
1202
+ seoAnalyzeDocument(documentId: string): Promise<any>;
1203
+ /** Compute a Flesch-Kincaid / grade-level readability score for a document body. */
1204
+ seoReadability(documentId: string): Promise<any>;
1205
+ /**
1206
+ * Suggest internal-link targets for a document. Title-keyword-based (extracts
1207
+ * up to 5 keywords > 3 chars from the doc title, matches them against
1208
+ * other PUBLISHED documents). Returns up to 10 suggestions. Distinct from
1209
+ * `suggestInternalLinks` above, which is content-based and freeform.
1210
+ *
1211
+ * `title` and `slug` are typed nullable because the underlying Prisma
1212
+ * `Document` schema makes them optional (`String?`) — the API forwards
1213
+ * the raw column values, so callers must handle nulls before rendering
1214
+ * anchor text or building target URLs.
1215
+ */
1216
+ seoInternalLinks(documentId: string): Promise<{
1217
+ suggestions: Array<{
1218
+ id: string;
1219
+ title: string | null;
1220
+ slug: string | null;
1221
+ collection: string;
1222
+ }>;
1223
+ }>;
1224
+ /** Build the OG / Twitter / canonical meta-tag preview for a document. */
1225
+ seoMetaPreview(documentId: string): Promise<any>;
1226
+ /** Build the Schema.org JSON-LD graph for a document. */
1227
+ seoSchemaGraph(documentId: string): Promise<any>;
1228
+ /**
1229
+ * Discover the registered section types + their content/field schemas.
1230
+ * Each entry carries `id`, `name`, `description`, `category`, `scope`,
1231
+ * `fields` (the inspector schema), and `defaultContent` / `defaultSettings`.
1232
+ *
1233
+ * Pass `scope: 'page'` or `scope: 'post'` to list only the types usable on
1234
+ * that surface (so an agent building a post template doesn't pick a
1235
+ * page-only type, and vice versa).
1236
+ */
1237
+ listSectionTypes(scope?: 'page' | 'post'): Promise<{
1238
+ data: any[];
1239
+ }>;
1240
+ /**
1241
+ * Validate section content against its type schema. Deterministic (no
1242
+ * LLM). Returns `{ valid, errors, warnings }` where each issue is
1243
+ * `{ sectionType, field?, message }`. An unknown `sectionType` is a hard
1244
+ * error (no registered renderer).
1245
+ */
1246
+ validateSection(input: {
1247
+ sectionType: string;
1248
+ content: Record<string, unknown>;
1249
+ settings?: Record<string, unknown>;
1250
+ }): Promise<{
1251
+ data: {
1252
+ valid: boolean;
1253
+ errors: Array<{
1254
+ sectionType: string;
1255
+ field?: string;
1256
+ message: string;
1257
+ }>;
1258
+ warnings: Array<{
1259
+ sectionType: string;
1260
+ field?: string;
1261
+ message: string;
1262
+ }>;
1263
+ };
1264
+ }>;
1265
+ /** List a page's ordered sections (`data.sections`). */
1266
+ listPageSections(collection: string, id: string): Promise<{
1267
+ data: any[];
1268
+ }>;
1269
+ /**
1270
+ * Add a section to a page. The server seeds registry defaults for
1271
+ * `sectionType`, shallow-merges the supplied `content` / `settings`,
1272
+ * generates an id, inserts at `position` (default end), and persists.
1273
+ * Returns `{ section, sections, validation }`.
1274
+ */
1275
+ addPageSection(collection: string, id: string, body: {
1276
+ sectionType: string;
1277
+ name?: string;
1278
+ internalLabel?: string;
1279
+ visible?: boolean;
1280
+ content?: Record<string, unknown>;
1281
+ settings?: Record<string, unknown>;
1282
+ position?: number;
1283
+ }): Promise<{
1284
+ data: {
1285
+ section: any;
1286
+ sections: any[];
1287
+ validation: any;
1288
+ };
1289
+ }>;
1290
+ /**
1291
+ * Update a section in place. Only supplied fields change; `content` /
1292
+ * `settings` are shallow-merged. Returns `{ section, sections, validation }`.
1293
+ */
1294
+ updatePageSection(collection: string, id: string, sectionId: string, body: {
1295
+ content?: Record<string, unknown>;
1296
+ settings?: Record<string, unknown>;
1297
+ name?: string;
1298
+ internalLabel?: string;
1299
+ visible?: boolean;
1300
+ }): Promise<{
1301
+ data: {
1302
+ section: any;
1303
+ sections: any[];
1304
+ validation: any;
1305
+ };
1306
+ }>;
1307
+ /** Reorder a page's sections to an explicit id order. Returns the new array. */
1308
+ reorderPageSections(collection: string, id: string, orderedIds: string[]): Promise<{
1309
+ data: any[];
1310
+ }>;
1311
+ /** Remove a section from a page. Returns the remaining sections. */
1312
+ removePageSection(collection: string, id: string, sectionId: string): Promise<{
1313
+ data: {
1314
+ sections: any[];
1315
+ };
1316
+ }>;
1317
+ /**
1318
+ * Read a post type's template: `{ postType, docId, header, sections }`.
1319
+ * Returns an unsaved default (centered header, empty section list) when
1320
+ * the type has no saved template yet (`docId: null`).
1321
+ */
1322
+ getPostTemplate(postType: string): Promise<{
1323
+ data: {
1324
+ postType: string;
1325
+ docId: string | null;
1326
+ header: any;
1327
+ sections: any[];
1328
+ };
1329
+ }>;
1330
+ /**
1331
+ * Create or replace a post type's template. `header` is coerced to a full
1332
+ * header config (missing keys filled with defaults) and `sections` are
1333
+ * coerced (unknown section types dropped) so the stored template always
1334
+ * renders. Use only `scope: 'post'` section types in `sections`.
1335
+ */
1336
+ savePostTemplate(postType: string, body: {
1337
+ header?: Record<string, unknown>;
1338
+ sections?: any[];
1339
+ }): Promise<{
1340
+ data: {
1341
+ postType: string;
1342
+ docId: string | null;
1343
+ header: any;
1344
+ sections: any[];
1345
+ };
1346
+ }>;
1347
+ }
1348
+ //# sourceMappingURL=client.d.ts.map