@cloudhal/mcp-server 0.0.0-stage → 0.1.1
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/LICENSE +202 -0
- package/README.md +18 -2
- package/dist/src/analytics-reports.d.ts +164 -0
- package/dist/src/analytics-reports.js +632 -0
- package/dist/src/analytics-tools.d.ts +4 -0
- package/dist/src/analytics-tools.js +597 -0
- package/dist/src/asset-tools.d.ts +5 -0
- package/dist/src/asset-tools.js +685 -0
- package/dist/src/body-tools.d.ts +46 -0
- package/dist/src/body-tools.js +708 -0
- package/dist/src/changes.d.ts +10 -0
- package/dist/src/changes.js +112 -0
- package/dist/src/chrome-tools.d.ts +8 -0
- package/dist/src/chrome-tools.js +207 -0
- package/dist/src/comment-tools.d.ts +9 -0
- package/dist/src/comment-tools.js +743 -0
- package/dist/src/comments.d.ts +184 -0
- package/dist/src/comments.js +408 -0
- package/dist/src/common.d.ts +258 -0
- package/dist/src/common.js +488 -0
- package/dist/src/content-shared.d.ts +50 -0
- package/dist/src/content-shared.js +102 -0
- package/dist/src/design-tools.d.ts +9 -0
- package/dist/src/design-tools.js +349 -0
- package/dist/src/elevation-tools.d.ts +3 -0
- package/dist/src/elevation-tools.js +331 -0
- package/dist/src/entry-terms.d.ts +14 -0
- package/dist/src/entry-terms.js +51 -0
- package/dist/src/entry-tools.d.ts +4 -0
- package/dist/src/entry-tools.js +442 -0
- package/dist/src/http.d.ts +48 -0
- package/dist/src/http.js +101 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.js +10 -0
- package/dist/src/menu-tools.d.ts +41 -0
- package/dist/src/menu-tools.js +1058 -0
- package/dist/src/page-tools.d.ts +5 -0
- package/dist/src/page-tools.js +311 -0
- package/dist/src/preview-doc.d.ts +9 -0
- package/dist/src/preview-doc.js +56 -0
- package/dist/src/preview-view.d.ts +19 -0
- package/dist/src/preview-view.js +71 -0
- package/dist/src/publish-tools.d.ts +10 -0
- package/dist/src/publish-tools.js +248 -0
- package/dist/src/render/cdp.d.ts +29 -0
- package/dist/src/render/cdp.js +190 -0
- package/dist/src/render/checks/components.d.ts +5 -0
- package/dist/src/render/checks/components.js +95 -0
- package/dist/src/render/checks/content.d.ts +5 -0
- package/dist/src/render/checks/content.js +63 -0
- package/dist/src/render/checks/contrast.d.ts +5 -0
- package/dist/src/render/checks/contrast.js +72 -0
- package/dist/src/render/checks/index.d.ts +1 -0
- package/dist/src/render/checks/index.js +29 -0
- package/dist/src/render/checks/layout.d.ts +5 -0
- package/dist/src/render/checks/layout.js +44 -0
- package/dist/src/render/checks/motion.d.ts +5 -0
- package/dist/src/render/checks/motion.js +66 -0
- package/dist/src/render/checks/navigation.d.ts +5 -0
- package/dist/src/render/checks/navigation.js +78 -0
- package/dist/src/render/checks/prelude.d.ts +1 -0
- package/dist/src/render/checks/prelude.js +64 -0
- package/dist/src/render/limiter.d.ts +11 -0
- package/dist/src/render/limiter.js +29 -0
- package/dist/src/render/page.d.ts +19 -0
- package/dist/src/render/page.js +135 -0
- package/dist/src/render/types.d.ts +89 -0
- package/dist/src/render/types.js +140 -0
- package/dist/src/render-tools.d.ts +14 -0
- package/dist/src/render-tools.js +314 -0
- package/dist/src/render.d.ts +14 -0
- package/dist/src/render.js +33 -0
- package/dist/src/schedule-tools.d.ts +2 -0
- package/dist/src/schedule-tools.js +1596 -0
- package/dist/src/schedules.d.ts +292 -0
- package/dist/src/schedules.js +890 -0
- package/dist/src/schema-transforms.d.ts +14 -0
- package/dist/src/schema-transforms.js +50 -0
- package/dist/src/search-tools.d.ts +2 -0
- package/dist/src/search-tools.js +112 -0
- package/dist/src/server-approvals.d.ts +51 -0
- package/dist/src/server-approvals.js +273 -0
- package/dist/src/server-call.d.ts +28 -0
- package/dist/src/server-call.js +259 -0
- package/dist/src/server-instructions.d.ts +4 -0
- package/dist/src/server-instructions.js +74 -0
- package/dist/src/server-resources.d.ts +4 -0
- package/dist/src/server-resources.js +83 -0
- package/dist/src/server-schema.d.ts +20 -0
- package/dist/src/server-schema.js +82 -0
- package/dist/src/server.d.ts +80 -0
- package/dist/src/server.js +135 -0
- package/dist/src/stdio.d.ts +9 -0
- package/dist/src/stdio.js +18 -0
- package/dist/src/surface-tools.d.ts +4 -0
- package/dist/src/surface-tools.js +357 -0
- package/dist/src/taxonomy-tools.d.ts +16 -0
- package/dist/src/taxonomy-tools.js +1433 -0
- package/dist/src/terms.d.ts +47 -0
- package/dist/src/terms.js +271 -0
- package/dist/src/tools.d.ts +39 -0
- package/dist/src/tools.js +511 -0
- package/dist/src/translation-tools.d.ts +3 -0
- package/dist/src/translation-tools.js +422 -0
- package/package.json +86 -3
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every tool module shares: the SDK-independent tool shape, the call context, `ToolError` and
|
|
3
|
+
* the mapping of store errors to it (stable codes, actionable text), and small schema/prompt helpers.
|
|
4
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
checkEntryLimit,
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
OwnLimitError,
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
} from "@cloudhal/agent-runtime";
|
|
19
|
+
|
|
20
|
+
import { CommentsError } from "@cloudhal/comments";
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
ConflictError,
|
|
25
|
+
FilterError,
|
|
26
|
+
InvariantError,
|
|
27
|
+
NotFoundError,
|
|
28
|
+
UniqueConstraintError,
|
|
29
|
+
} from "@cloudhal/core-ts";
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
import { ScheduleConflictError, ScheduleError } from "@cloudhal/schedules";
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
import { ScheduleRequestError, } from "./schedules.js";
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
/** A content block of a tool result (the MCP shapes Cloudhal uses). */
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
/** Media settings of a deployment for the asset tools (RFC-0004 §5.6–§5.7). */
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
/** What `render_page` renders with (RFC-0021 §4). */
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Watches drafts being built (RFC-0020 §6): Studio's Build view follows a page while agents
|
|
149
|
+
* change it. Called after a write succeeds, and when a build's operations are refused, so a
|
|
150
|
+
* person sees the agent correct itself. Awaited through `notifyBuild`, which bounds and swallows
|
|
151
|
+
* failures: watching never fails or slows a write noticeably.
|
|
152
|
+
*/
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
/** Tells the Build view, waiting at most 2 s; a failure is logged, never thrown (RFC-0020 §6). */
|
|
173
|
+
export async function notifyBuild(report ) {
|
|
174
|
+
try {
|
|
175
|
+
const pending = report();
|
|
176
|
+
if (!pending) return;
|
|
177
|
+
let timer ;
|
|
178
|
+
await Promise.race([
|
|
179
|
+
pending,
|
|
180
|
+
new Promise ((resolve) => {
|
|
181
|
+
timer = setTimeout(resolve, 2_000);
|
|
182
|
+
}),
|
|
183
|
+
]).finally(() => clearTimeout(timer));
|
|
184
|
+
} catch (e) {
|
|
185
|
+
console.warn(`cloudhal: build view not told: ${e instanceof Error ? e.message : String(e)}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Without a membership source an agent owns what it wrote itself (local stdio with own-limited
|
|
246
|
+
* scopes); people are themselves; the system owns nothing.
|
|
247
|
+
*/
|
|
248
|
+
export const ACTOR_OWNERSHIP = {
|
|
249
|
+
async personOf(actor) {
|
|
250
|
+
return actor.kind === "system" ? undefined : actor.id;
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
/** The person a call is measured against for "own" (RFC-0005 §4). */
|
|
255
|
+
export function callerPerson(ctx ) {
|
|
256
|
+
return ctx.principal.person?.id ?? ctx.principal.actor.id;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Checks an entry write against the call's own limit (I9); refusals are `denied`. */
|
|
260
|
+
export async function entryLimitCheck(
|
|
261
|
+
ctx ,
|
|
262
|
+
op ,
|
|
263
|
+
stored ,
|
|
264
|
+
next ,
|
|
265
|
+
) {
|
|
266
|
+
try {
|
|
267
|
+
await checkEntryLimit({
|
|
268
|
+
limit: ctx.limit,
|
|
269
|
+
op,
|
|
270
|
+
person: callerPerson(ctx),
|
|
271
|
+
owner: ctx.ownership ?? ACTOR_OWNERSHIP,
|
|
272
|
+
stored,
|
|
273
|
+
next,
|
|
274
|
+
});
|
|
275
|
+
} catch (e) {
|
|
276
|
+
if (e instanceof OwnLimitError) throw new ToolError("denied", e.message, { code: e.code });
|
|
277
|
+
throw e;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Stable error codes agents can branch on; the text says what to do next. */
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
/** What an approval request would change (RFC-0014 §5): a body, block by block. */
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
export class ToolError extends Error {
|
|
319
|
+
name = "ToolError";
|
|
320
|
+
code ;
|
|
321
|
+
/** Machine-readable context (suggestions, counts, ids). */
|
|
322
|
+
details ;
|
|
323
|
+
constructor(code , message , details = {}) {
|
|
324
|
+
super(message);
|
|
325
|
+
this.code = code;
|
|
326
|
+
this.details = details;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export const id = { type: "string", pattern: "^[a-z]+_[A-Za-z0-9]{6,32}$" };
|
|
331
|
+
export const SLUG_PATTERN = "^[a-z0-9]+(?:-[a-z0-9]+)*$";
|
|
332
|
+
export const str = (desc , extra = {}) => ({
|
|
333
|
+
type: "string",
|
|
334
|
+
description: desc,
|
|
335
|
+
...extra,
|
|
336
|
+
});
|
|
337
|
+
export const obj = (
|
|
338
|
+
properties ,
|
|
339
|
+
required = [],
|
|
340
|
+
extra = {},
|
|
341
|
+
) => ({ type: "object", additionalProperties: false, properties, required, ...extra });
|
|
342
|
+
export const strip = (schema ) => {
|
|
343
|
+
const { $schema: _s, $id: _i, ...rest } = schema ;
|
|
344
|
+
return rest;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
/** Agent-controlled text in approval prompts is shortened and quoted so it cannot pose as UI. */
|
|
348
|
+
export function quote(text , max = 60) {
|
|
349
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
350
|
+
return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export async function requireSite(ctx , siteId ) {
|
|
354
|
+
const site = await ctx.store.getSite(siteId);
|
|
355
|
+
if (!site) throw new ToolError("not_found", `site ${siteId} not found`);
|
|
356
|
+
return site;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export async function autoSnapshot(
|
|
360
|
+
ctx ,
|
|
361
|
+
siteId ,
|
|
362
|
+
tool ,
|
|
363
|
+
metadata ,
|
|
364
|
+
) {
|
|
365
|
+
return ctx.store.createSnapshot(siteId, {
|
|
366
|
+
actor: ctx.principal.actor,
|
|
367
|
+
reason: "agentMutation",
|
|
368
|
+
label: `${tool} by ${ctx.principal.actor.name ?? ctx.principal.actor.id}`,
|
|
369
|
+
...(metadata ? { metadata } : {}),
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** What to do next, per invariant (RFC-0002 §3). */
|
|
374
|
+
function invariantHint(e ) {
|
|
375
|
+
const key = typeof e.details.taxonomy === "string" ? e.details.taxonomy : undefined;
|
|
376
|
+
switch (e.code) {
|
|
377
|
+
case "required_term":
|
|
378
|
+
return key
|
|
379
|
+
? `this site requires a ${key} on review and published articles; call list_terms(taxonomy: "${key}") and set terms.${key}.primary`
|
|
380
|
+
: "merge the term into another one (merge_terms) or reassign those articles first";
|
|
381
|
+
case "primary_not_assigned":
|
|
382
|
+
return "the primary term must be one of the assigned terms; set it with terms.<taxonomy>.primary (it is added for you)";
|
|
383
|
+
case "invalid_assignment":
|
|
384
|
+
return "call list_taxonomies for the keys and collections, and list_terms or resolve_terms for the terms";
|
|
385
|
+
case "term_not_assignable":
|
|
386
|
+
return "the term was retired or merged; call resolve_terms with its name to find the term that replaced it";
|
|
387
|
+
case "too_many_terms":
|
|
388
|
+
return "list_taxonomies shows each taxonomy's maxPerEntry; drop some with terms.<taxonomy>.remove";
|
|
389
|
+
case "not_allowed":
|
|
390
|
+
return "ask a person to add or review it in Cloudhal Studio, or reuse an existing term (list_terms)";
|
|
391
|
+
case "slug_taken":
|
|
392
|
+
return "reuse that term (resolve_terms) or choose another slug";
|
|
393
|
+
case "invalid_labels":
|
|
394
|
+
return "names and synonyms must not repeat another term's name, synonym or slug; reuse that term or pick another name";
|
|
395
|
+
case "too_large":
|
|
396
|
+
return typeof e.details.limit === "number"
|
|
397
|
+
? "use a smaller file; larger files are uploaded by a person in Studio"
|
|
398
|
+
: "split the operation, or ask a person for the staged job";
|
|
399
|
+
case "unknown_asset":
|
|
400
|
+
return "call list_assets for this site's files, or add one with upload_asset or fetch_asset_url";
|
|
401
|
+
case "retired_asset":
|
|
402
|
+
return "pick another file (list_assets), or ask a person to restore this one";
|
|
403
|
+
case "in_use":
|
|
404
|
+
return "remove those uses first (update_entry, update_page), or retire the file instead";
|
|
405
|
+
case "fetch_blocked":
|
|
406
|
+
return "use a public https address of the file itself; internal hosts, IP addresses and redirects into them are refused";
|
|
407
|
+
case "reserved_path":
|
|
408
|
+
return "paths under /media/<16 hex digits>/ belong to media files";
|
|
409
|
+
case "retry":
|
|
410
|
+
return "call the tool again in a few seconds";
|
|
411
|
+
case "locale_not_enabled":
|
|
412
|
+
return "use one of the site's locales (get_site), or ask a person to add the language";
|
|
413
|
+
case "translation_exists":
|
|
414
|
+
return "that language already has a version in the group; update it instead";
|
|
415
|
+
case "review_required":
|
|
416
|
+
return "save it as a draft; a person reviews machine translations of this collection before they publish";
|
|
417
|
+
case "host_taken":
|
|
418
|
+
return "pick another hostname; this one already serves another site of the installation";
|
|
419
|
+
case "invalid_surface":
|
|
420
|
+
return "fix the parts named by the JSON Pointers in details.issues; get_catalog lists every component, its props and the binding rules";
|
|
421
|
+
default:
|
|
422
|
+
return undefined;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Store errors as `ToolError`s with stable codes; anything else is returned unchanged. */
|
|
427
|
+
export function toToolError(e ) {
|
|
428
|
+
if (e instanceof ToolError) return e;
|
|
429
|
+
// Schedule errors (RFC-0008): a refused input keeps its code in the text and the details.
|
|
430
|
+
if (e instanceof ScheduleError)
|
|
431
|
+
return new ToolError(
|
|
432
|
+
e.code === "not_found" ? "not_found" : "invalid",
|
|
433
|
+
`${e.code}: ${e.message}`,
|
|
434
|
+
{
|
|
435
|
+
code: e.code,
|
|
436
|
+
},
|
|
437
|
+
);
|
|
438
|
+
if (e instanceof ScheduleConflictError)
|
|
439
|
+
return new ToolError(
|
|
440
|
+
"conflict",
|
|
441
|
+
`${e.message}; read it again (get_release) and pass the current rev`,
|
|
442
|
+
{ kind: e.kind, id: e.id },
|
|
443
|
+
);
|
|
444
|
+
if (e instanceof ScheduleRequestError) return new ToolError("invalid", e.message);
|
|
445
|
+
// Comment store errors (RFC-0007): stable codes, the store's message says what to fix.
|
|
446
|
+
if (e instanceof CommentsError)
|
|
447
|
+
return new ToolError(
|
|
448
|
+
e.code === "not_found"
|
|
449
|
+
? "not_found"
|
|
450
|
+
: e.code === "conflict"
|
|
451
|
+
? "conflict"
|
|
452
|
+
: e.code === "forbidden"
|
|
453
|
+
? "denied"
|
|
454
|
+
: "invalid",
|
|
455
|
+
e.message,
|
|
456
|
+
);
|
|
457
|
+
if (e instanceof ConflictError) return new ToolError("conflict", e.message);
|
|
458
|
+
if (e instanceof NotFoundError) return new ToolError("not_found", e.message);
|
|
459
|
+
if (e instanceof UniqueConstraintError)
|
|
460
|
+
return new ToolError(
|
|
461
|
+
"conflict",
|
|
462
|
+
e.kind === "contentEntry"
|
|
463
|
+
? `an article with this collection, locale and slug already exists (${e.key}); choose another slug`
|
|
464
|
+
: `${e.kind} "${e.key}" already exists`,
|
|
465
|
+
);
|
|
466
|
+
if (e instanceof InvariantError) {
|
|
467
|
+
const hint = invariantHint(e);
|
|
468
|
+
return new ToolError(e.code, hint ? `${e.message}. ${hint}` : e.message, { ...e.details });
|
|
469
|
+
}
|
|
470
|
+
if (e instanceof FilterError) {
|
|
471
|
+
if (e.code === "unknown_term")
|
|
472
|
+
return new ToolError(
|
|
473
|
+
"unknown_term",
|
|
474
|
+
`${e.message}; call list_terms for the vocabulary (old slugs still match)`,
|
|
475
|
+
{ unknown: e.unknown },
|
|
476
|
+
);
|
|
477
|
+
if (e.code === "unknown_taxonomy") return new ToolError("unknown_taxonomy", e.message);
|
|
478
|
+
return new ToolError(
|
|
479
|
+
"invalid_filter",
|
|
480
|
+
`${e.message} (grammar: clauses joined by "+", e.g. "category:news+tag:[ai,llm]+-tag:sponsored"; "primary:category:x"; "category:x>*" for sub-categories)`,
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
return e;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export function mapError(e ) {
|
|
487
|
+
throw toToolError(e);
|
|
488
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Schemas and helpers shared by the site, page, surface, entry and publish tool families. */
|
|
2
|
+
import type { Actor, Asset, ContentEntry, Page, Site } from "@cloudhal/core-contracts";
|
|
3
|
+
import type { BlockWriter } from "@cloudhal/core-ts";
|
|
4
|
+
import { unknownBlockTypes } from "@cloudhal/core-ts";
|
|
5
|
+
import type { BlockRegistry } from "@cloudhal/core-ts/blocks";
|
|
6
|
+
import { type Catalog } from "@cloudhal/core-ts/surfaces";
|
|
7
|
+
import { type JsonSchema, type ToolContext } from "./common.js";
|
|
8
|
+
/** The catalog surfaces on this site are written against: the theme's name and its block types. */
|
|
9
|
+
export declare function siteCatalog(site: Site, ctx: {
|
|
10
|
+
store: {
|
|
11
|
+
blocks: BlockRegistry;
|
|
12
|
+
};
|
|
13
|
+
}): Catalog;
|
|
14
|
+
/** Body blocks an agent changes carry it and the MCP channel (RFC-0014 §3). */
|
|
15
|
+
export declare function mcpWriter(ctx: {
|
|
16
|
+
principal: {
|
|
17
|
+
actor: Actor;
|
|
18
|
+
};
|
|
19
|
+
}): BlockWriter;
|
|
20
|
+
export declare const SITE: JsonSchema;
|
|
21
|
+
export declare const PAGE: JsonSchema;
|
|
22
|
+
export declare const ENTRY: JsonSchema;
|
|
23
|
+
export declare const SNAPSHOT: JsonSchema;
|
|
24
|
+
export declare const SECTION: JsonSchema;
|
|
25
|
+
export type Seo = NonNullable<Page["seo"]>;
|
|
26
|
+
/** An article's cover image by asset (RFC-0004): `{ assetId, alt? }`; the store fills in the rest. */
|
|
27
|
+
export declare const COVER_INPUT: JsonSchema;
|
|
28
|
+
/** The tool input for a page's sections. */
|
|
29
|
+
export declare const sectionInput: {
|
|
30
|
+
type: string;
|
|
31
|
+
items: JsonSchema;
|
|
32
|
+
description: string;
|
|
33
|
+
};
|
|
34
|
+
/** The media-library assets a page or article refers to, by id, so previews render them (RFC-0004). */
|
|
35
|
+
export declare function assetsOf(ctx: ToolContext, kind: "page" | "contentEntry", node: Page | ContentEntry): Promise<Map<string, Asset>>;
|
|
36
|
+
export declare function coverImage(input: unknown): NonNullable<ContentEntry["coverImage"]>;
|
|
37
|
+
/** Reported when a write keeps blocks of types this deployment does not know (RFC-0015 §7). */
|
|
38
|
+
export declare const unknownBlocks: {
|
|
39
|
+
type: string;
|
|
40
|
+
items: {
|
|
41
|
+
type: string;
|
|
42
|
+
};
|
|
43
|
+
description: string;
|
|
44
|
+
};
|
|
45
|
+
export declare function unknownBlocksOf(ctx: ToolContext, content: Parameters<typeof unknownBlockTypes>[1]): {
|
|
46
|
+
unknownBlocks?: string[];
|
|
47
|
+
};
|
|
48
|
+
export declare const entryMarkdown: (entry: ContentEntry) => string | undefined;
|
|
49
|
+
/** Agents write markdown only; raw HTML sections are for people in Studio (security review H3). */
|
|
50
|
+
export declare function assertNoHtmlSections(sections: unknown): void;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** Schemas and helpers shared by the site, page, surface, entry and publish tool families. */
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
ContentEntrySchema,
|
|
6
|
+
PageSchema,
|
|
7
|
+
SectionSchema,
|
|
8
|
+
SiteSchema,
|
|
9
|
+
SnapshotSchema,
|
|
10
|
+
} from "@cloudhal/core-contracts";
|
|
11
|
+
|
|
12
|
+
import { unknownBlockTypes } from "@cloudhal/core-ts";
|
|
13
|
+
|
|
14
|
+
import { assetRefsOf } from "@cloudhal/core-ts/media";
|
|
15
|
+
import { createCatalog } from "@cloudhal/core-ts/surfaces";
|
|
16
|
+
import { id, str, strip, ToolError } from "./common.js";
|
|
17
|
+
|
|
18
|
+
/** The catalog surfaces on this site are written against: the theme's name and its block types. */
|
|
19
|
+
export function siteCatalog(site , ctx ) {
|
|
20
|
+
return createCatalog({ name: site.theme.name, blocks: ctx.store.blocks });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Body blocks an agent changes carry it and the MCP channel (RFC-0014 §3). */
|
|
24
|
+
export function mcpWriter(ctx ) {
|
|
25
|
+
return { actor: ctx.principal.actor, via: "mcp" };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const SITE = strip(SiteSchema);
|
|
29
|
+
export const PAGE = strip(PageSchema);
|
|
30
|
+
export const ENTRY = strip(ContentEntrySchema);
|
|
31
|
+
export const SNAPSHOT = strip(SnapshotSchema);
|
|
32
|
+
export const SECTION = strip(SectionSchema);
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
/** An article's cover image by asset (RFC-0004): `{ assetId, alt? }`; the store fills in the rest. */
|
|
36
|
+
export const COVER_INPUT = {
|
|
37
|
+
type: "object",
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
required: ["assetId"],
|
|
40
|
+
description:
|
|
41
|
+
"Cover image from the media library (list_assets, upload_asset). alt overrides the file's own alt text for this article.",
|
|
42
|
+
properties: { assetId: id, alt: str("Alt text for this use", { maxLength: 500 }) },
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** The tool input for a page's sections. */
|
|
46
|
+
export const sectionInput = {
|
|
47
|
+
type: "array",
|
|
48
|
+
items: SECTION,
|
|
49
|
+
description:
|
|
50
|
+
'Ordered sections; keep existing ids when editing. Typed blocks (a callout, a button, an FAQ, the theme\'s own) are sections with kind "component", a type and its props: list_block_types.',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** The media-library assets a page or article refers to, by id, so previews render them (RFC-0004). */
|
|
54
|
+
export async function assetsOf(
|
|
55
|
+
ctx ,
|
|
56
|
+
kind ,
|
|
57
|
+
node ,
|
|
58
|
+
) {
|
|
59
|
+
const ids = [...new Set(assetRefsOf(kind, node).map((r) => r.assetId))];
|
|
60
|
+
const found = await Promise.all(ids.map((id) => ctx.store.getAsset(id)));
|
|
61
|
+
return new Map(
|
|
62
|
+
found
|
|
63
|
+
.filter((a) => a !== undefined && a.siteId === node.siteId)
|
|
64
|
+
.map((a) => [a.id, a]),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function coverImage(input ) {
|
|
69
|
+
const c = input ;
|
|
70
|
+
// `src` is written by the store from the asset (invariant 8).
|
|
71
|
+
return { assetId: c.assetId, alt: c.alt ?? "", src: `asset:${c.assetId}` };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Reported when a write keeps blocks of types this deployment does not know (RFC-0015 §7). */
|
|
75
|
+
export const unknownBlocks = {
|
|
76
|
+
type: "array",
|
|
77
|
+
items: { type: "string" },
|
|
78
|
+
description:
|
|
79
|
+
"Block types used here that this site does not know (another theme's?): kept as written, not shown on the site. list_block_types shows the known ones.",
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export function unknownBlocksOf(
|
|
83
|
+
ctx ,
|
|
84
|
+
content ,
|
|
85
|
+
) {
|
|
86
|
+
const names = unknownBlockTypes(ctx.store.blocks, content);
|
|
87
|
+
return names.length ? { unknownBlocks: names } : {};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const entryMarkdown = (entry ) =>
|
|
91
|
+
entry.body.format === "markdown" ? entry.body.value : undefined;
|
|
92
|
+
|
|
93
|
+
/** Agents write markdown only; raw HTML sections are for people in Studio (security review H3). */
|
|
94
|
+
export function assertNoHtmlSections(sections ) {
|
|
95
|
+
if (!Array.isArray(sections)) return;
|
|
96
|
+
for (const s of sections )
|
|
97
|
+
if (s?.kind === "richText" && s.format !== "markdown")
|
|
98
|
+
throw new ToolError(
|
|
99
|
+
"invalid",
|
|
100
|
+
`section ${s.id ?? ""} uses format "${s.format}"; agents may only write markdown sections`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DESIGN.md over MCP (RFC-0021 §2): `get_design` gives an agent everything it needs before it
|
|
3
|
+
* designs (the theme's and the site's design rules, the design settings, the catalog's names and
|
|
4
|
+
* Cloudhal's guidance) in one call; `set_design` changes the site's rules and always waits for a
|
|
5
|
+
* person. The site's rules are owner-written data, delimited in the text result: they shape design
|
|
6
|
+
* choices and never grant permissions.
|
|
7
|
+
*/
|
|
8
|
+
import { type ToolDefinition } from "./common.js";
|
|
9
|
+
export declare function designTools(): ToolDefinition[];
|