@sudajs/cli 0.10.0 → 0.10.2

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/dist/index.js CHANGED
@@ -265,6 +265,123 @@ var activateThemeOutputSchema = z.discriminatedUnion("status", [
265
265
  themeVersion: z.string().nullable()
266
266
  })
267
267
  ]);
268
+ var contactOptionSchema = z.object({ label: z.string().min(1).max(80), value: z.string().min(1).max(80) });
269
+ var contactFieldSchema = z.object({
270
+ id: z.string().trim().min(1).max(48).regex(/^[a-zA-Z][a-zA-Z0-9_-]*$/),
271
+ type: z.enum(["text", "textarea", "checkbox", "radio", "select"]),
272
+ label: z.string().trim().min(1).max(120),
273
+ placeholder: z.string().trim().max(160).optional(),
274
+ required: z.boolean().default(false),
275
+ options: z.array(contactOptionSchema).default([]),
276
+ validation: z.object({
277
+ format: z.enum(["email"]).optional(),
278
+ maxLength: z.number().int().min(1).max(5e3).optional()
279
+ }).default({})
280
+ });
281
+ function addUniqueContactIssue(items, collectionName, key, ctx, prefix = []) {
282
+ const seen = /* @__PURE__ */ new Set();
283
+ for (const [index, item] of items.entries()) {
284
+ const value = item[key];
285
+ if (value === void 0 || value === null || value === "") continue;
286
+ if (seen.has(value)) {
287
+ ctx.addIssue({
288
+ code: z.ZodIssueCode.custom,
289
+ message: `Duplicate ${String(key)} in ${collectionName}.`,
290
+ path: [...prefix, index, key]
291
+ });
292
+ continue;
293
+ }
294
+ seen.add(value);
295
+ }
296
+ }
297
+ var contactFieldsSchema = z.array(contactFieldSchema).superRefine((fields, ctx) => {
298
+ addUniqueContactIssue(fields, "fields", "id", ctx);
299
+ for (const [index, field] of fields.entries()) {
300
+ if ((field.type === "radio" || field.type === "select") && field.options.length === 0) {
301
+ ctx.addIssue({
302
+ code: z.ZodIssueCode.custom,
303
+ message: `${field.type} fields require at least one option.`,
304
+ path: [index, "options"]
305
+ });
306
+ }
307
+ addUniqueContactIssue(field.options, "options", "value", ctx, [index, "options"]);
308
+ }
309
+ });
310
+ var contactNotificationUpdateSchema = z.object({
311
+ id: z.string().trim().min(1).max(48),
312
+ type: z.enum(["email", "slack", "wecom", "feishu", "dingtalk"]),
313
+ enabled: z.boolean().default(true),
314
+ label: z.string().trim().min(1).max(80),
315
+ recipients: z.array(z.string().trim().email()).default([]),
316
+ url: z.string().trim().url().optional()
317
+ });
318
+ var contactWebhookUpdateSchema = z.object({
319
+ id: z.string().trim().min(1).max(48),
320
+ enabled: z.boolean().default(true),
321
+ label: z.string().trim().min(1).max(80),
322
+ url: z.string().trim().url().optional()
323
+ });
324
+ var updateContactFormInputSchema = z.object({
325
+ projectId: z.string().min(1),
326
+ enabled: z.boolean().optional(),
327
+ title: z.string().trim().min(1).max(120).optional(),
328
+ successMessage: z.string().trim().min(1).max(240).optional(),
329
+ submitLabel: z.string().trim().min(1).max(80).optional(),
330
+ fields: contactFieldsSchema.optional(),
331
+ notifications: z.array(contactNotificationUpdateSchema).optional(),
332
+ webhooks: z.array(contactWebhookUpdateSchema).optional(),
333
+ confirm: z.boolean().optional()
334
+ });
335
+ var contactFormViewSchema = z.object({
336
+ enabled: z.boolean(),
337
+ title: z.string(),
338
+ successMessage: z.string(),
339
+ submitLabel: z.string(),
340
+ fields: z.array(contactFieldSchema),
341
+ notifications: z.array(
342
+ z.object({
343
+ id: z.string(),
344
+ type: z.enum(["email", "slack", "wecom", "feishu", "dingtalk"]),
345
+ enabled: z.boolean(),
346
+ label: z.string(),
347
+ recipients: z.array(z.string()),
348
+ urlMasked: z.string().nullable()
349
+ })
350
+ ),
351
+ webhooks: z.array(
352
+ z.object({
353
+ id: z.string(),
354
+ enabled: z.boolean(),
355
+ label: z.string(),
356
+ urlMasked: z.string().nullable()
357
+ })
358
+ ),
359
+ limits: z.object({
360
+ maxFields: z.number(),
361
+ maxNotificationChannels: z.number(),
362
+ maxWebhooks: z.number()
363
+ }),
364
+ features: z.object({
365
+ contactNotifications: z.boolean(),
366
+ contactWebhooks: z.boolean()
367
+ })
368
+ });
369
+ var contactFormOutputSchema = z.object({
370
+ contactForm: contactFormViewSchema
371
+ });
372
+ var updateContactFormOutputSchema = z.discriminatedUnion("status", [
373
+ z.object({
374
+ status: z.literal("needs_confirmation"),
375
+ projectId: z.string(),
376
+ impact: z.string(),
377
+ draft: updateContactFormInputSchema.omit({ confirm: true })
378
+ }),
379
+ z.object({
380
+ status: z.literal("updated"),
381
+ projectId: z.string(),
382
+ contactForm: contactFormViewSchema
383
+ })
384
+ ]);
268
385
  var PROJECT_NAME_MIN_LENGTH = 1;
269
386
  var PROJECT_NAME_MAX_LENGTH = 80;
270
387
  var PROJECT_DESCRIPTION_MIN_LENGTH = 1;
@@ -796,6 +913,8 @@ async function buildClientRuntime(root, minify) {
796
913
  if (!await pathExists(entryPoint)) {
797
914
  throw new Error(`Missing client entry: ${entryPoint}`);
798
915
  }
916
+ const packageJson = JSON.parse(await readFile(path2.join(root, "package.json"), "utf8"));
917
+ const themeVersion = typeof packageJson.version === "string" ? packageJson.version : "0.0.0";
799
918
  await mkdir(path2.join(root, "dist"), { recursive: true });
800
919
  const hostReactShimPlugin = createHostReactShimPlugin();
801
920
  const themeEngineRuntimeAliasPlugin = createThemeEngineRuntimeAliasPlugin(root);
@@ -816,6 +935,9 @@ async function buildClientRuntime(root, minify) {
816
935
  minify,
817
936
  outfile: path2.join(root, "dist", "runtime.client.js"),
818
937
  platform: "browser",
938
+ define: {
939
+ __SUDA_THEME_VERSION__: JSON.stringify(themeVersion)
940
+ },
819
941
  plugins: [hostReactShimPlugin, themeEngineRuntimeAliasPlugin, themeDependencyResolverPlugin],
820
942
  sourcemap: false,
821
943
  target: "es2022",
@@ -963,6 +1085,7 @@ var __testUtils = {
963
1085
  initThemeWithKey,
964
1086
  performActivateTheme,
965
1087
  performConfirmedPageOperation,
1088
+ performUpdateContactForm,
966
1089
  renderDevStarterPageHtml,
967
1090
  resolveScreenshotOptions,
968
1091
  slugifyThemeName,
@@ -1638,6 +1761,58 @@ async function performActivateTheme(auth, input, fetcher = fetchJson) {
1638
1761
  themeVersion: response.themeVersion
1639
1762
  });
1640
1763
  }
1764
+ async function getRemoteContactForm(auth, projectId, fetcher = fetchJson) {
1765
+ const response = await fetcher(
1766
+ `${auth.baseUrl}/api/cli/agent/projects/${encodeURIComponent(projectId)}/contact-form`,
1767
+ { token: auth.token }
1768
+ );
1769
+ return contactFormViewSchema.parse(response.contactForm);
1770
+ }
1771
+ async function updateRemoteContactForm(auth, input, fetcher = fetchJson) {
1772
+ const body = {};
1773
+ if (input.enabled !== void 0) body.enabled = input.enabled;
1774
+ if (input.title !== void 0) body.title = input.title;
1775
+ if (input.successMessage !== void 0) body.successMessage = input.successMessage;
1776
+ if (input.submitLabel !== void 0) body.submitLabel = input.submitLabel;
1777
+ if (input.fields !== void 0) body.fields = input.fields;
1778
+ if (input.notifications !== void 0) body.notifications = input.notifications;
1779
+ if (input.webhooks !== void 0) body.webhooks = input.webhooks;
1780
+ const response = await fetcher(
1781
+ `${auth.baseUrl}/api/cli/agent/projects/${encodeURIComponent(input.projectId)}/contact-form`,
1782
+ {
1783
+ method: "PATCH",
1784
+ token: auth.token,
1785
+ headers: { "Content-Type": "application/json" },
1786
+ body: JSON.stringify(body)
1787
+ }
1788
+ );
1789
+ return contactFormViewSchema.parse(response.contactForm);
1790
+ }
1791
+ async function performUpdateContactForm(auth, input, fetcher = fetchJson) {
1792
+ const draft = updateContactFormInputSchema.omit({ confirm: true }).parse(input);
1793
+ const changes = [
1794
+ input.enabled !== void 0 ? "enabled state" : null,
1795
+ input.title !== void 0 || input.successMessage !== void 0 || input.submitLabel !== void 0 ? "form copy" : null,
1796
+ input.fields !== void 0 ? `${input.fields.length} field(s)` : null,
1797
+ input.notifications !== void 0 ? `${input.notifications.length} notification channel(s)` : null,
1798
+ input.webhooks !== void 0 ? `${input.webhooks.length} webhook(s)` : null
1799
+ ].filter((item) => typeof item === "string");
1800
+ const impact = `This will update the project contact form settings for "${input.projectId}": ${changes.join(", ") || "no explicit changes"}. Contact form fields affect what visitors submit publicly; notifications and webhooks may send future submissions to external systems and are subject to plan features and limits.`;
1801
+ if (input.confirm !== true) {
1802
+ return {
1803
+ status: "needs_confirmation",
1804
+ projectId: input.projectId,
1805
+ impact,
1806
+ draft
1807
+ };
1808
+ }
1809
+ const contactForm = await updateRemoteContactForm(auth, input, fetcher);
1810
+ return {
1811
+ status: "updated",
1812
+ projectId: input.projectId,
1813
+ contactForm
1814
+ };
1815
+ }
1641
1816
  async function listAgentProjects() {
1642
1817
  const auth = await requireCliBaseUrl();
1643
1818
  const result = await fetchJson(
@@ -1691,7 +1866,7 @@ async function startMcpServer() {
1691
1866
  server.registerTool(
1692
1867
  "create_project",
1693
1868
  {
1694
- description: "Create a new Suda project for the current user. Only call this after list_projects returned an empty list. Conversation flow before calling: (1) Ask the user for the website / brand name in one turn and capture it as `name`. (2) Ask the user for a business introduction; the user is allowed (and expected) to answer over multiple turns \u2014 keep asking follow-up questions and accumulating their answers until they confirm they are done, then concatenate the accumulated text into a single `siteDescription` (trimmed). Do not invent details the user did not provide. Do not call this tool until both fields are confirmed by the user. The project is created with the default theme and starter pages; no in-app AI site generation is triggered \u2014 drive page content afterwards via create_page_draft using the returned projectId.",
1869
+ description: "Create a new Suda project for the current user. Only call this after list_projects returned an empty list. Conversation flow before calling: (1) Ask the user for the website / brand name in one turn and capture it as `name`. (2) Ask the user for a business introduction; the user is allowed (and expected) to answer over multiple turns \u2014 keep asking follow-up questions and accumulating their answers until they confirm they are done, then concatenate the accumulated text into a single `siteDescription` (trimmed). Do not invent details the user did not provide. Do not call this tool until both fields are confirmed by the user. The project is created with the default theme, starter pages, and a default project contact form. After creation, if the business context suggests better contact fields, call get_contact_form_settings, propose the contact form plan to the user, and only call update_contact_form_settings with confirm:true after they agree. No in-app AI site generation is triggered \u2014 drive page content afterwards via create_page_draft using the returned projectId.",
1695
1870
  inputSchema: {
1696
1871
  name: createProjectInputSchema.shape.name.describe(
1697
1872
  "Website or brand name confirmed by the user in a dedicated turn. Used as the project name and site title."
@@ -1764,7 +1939,7 @@ async function startMcpServer() {
1764
1939
  server.registerTool(
1765
1940
  "get_page_schema",
1766
1941
  {
1767
- description: "Return the AI-first output schema for generating Suda page content, including all section schemas. Field schemas include dynamic visibility metadata for Suda visibleIf fields.",
1942
+ description: "Return the AI-first output schema for generating Suda page content, including all section schemas. Field schemas include dynamic visibility metadata for Suda visibleIf fields. If the theme has a contact section, generate only the section placement/content props; public form fields and delivery integrations are project contact form settings exposed to themes as metadata.contactForm. Use get_contact_form_settings and update_contact_form_settings for user-approved contact form changes.",
1768
1943
  inputSchema: {
1769
1944
  theme: z.string(),
1770
1945
  version: z.string().optional(),
@@ -1785,7 +1960,7 @@ async function startMcpServer() {
1785
1960
  server.registerTool(
1786
1961
  "get_section_schema",
1787
1962
  {
1788
- description: "Return the AI-first output schema for one section component in a theme. Field schemas include dynamic visibility metadata for Suda visibleIf fields.",
1963
+ description: "Return the AI-first output schema for one section component in a theme. Field schemas include dynamic visibility metadata for Suda visibleIf fields. For contact sections, use the schema for presentation props only and do not hardcode form field metadata, notification channels, webhook URLs, or recipients into page content. Use contact form tools for project-level contact settings.",
1789
1964
  inputSchema: {
1790
1965
  theme: z.string(),
1791
1966
  section: z.string(),
@@ -1822,6 +1997,48 @@ async function startMcpServer() {
1822
1997
  return mcpStructured("Suda page content validation result.", structuredContent);
1823
1998
  }
1824
1999
  );
2000
+ server.registerTool(
2001
+ "get_contact_form_settings",
2002
+ {
2003
+ description: "Read project contact form settings. Use this before planning contact form changes. The result includes editable field metadata, masked notification/webhook URLs, and plan features/limits. Secrets are never returned.",
2004
+ inputSchema: {
2005
+ projectId: z.string()
2006
+ },
2007
+ outputSchema: contactFormOutputSchema.shape
2008
+ },
2009
+ async ({ projectId }) => {
2010
+ const auth = await requireCliBaseUrl();
2011
+ const structuredContent = contactFormOutputSchema.parse({
2012
+ contactForm: await getRemoteContactForm(auth, projectId)
2013
+ });
2014
+ return mcpStructured("Suda contact form settings.", structuredContent);
2015
+ }
2016
+ );
2017
+ server.registerTool(
2018
+ "update_contact_form_settings",
2019
+ {
2020
+ description: "Plan or update project contact form settings. Use this when the user asks to change contact form fields, labels, validation, notification channels, or webhooks. Supported field types are text, textarea, checkbox, radio, and select. Conversation flow: first call get_contact_form_settings, then propose the exact draft to the user. Call this tool without confirm or with confirm:false to return the impact summary; only call again with confirm:true after the user explicitly agrees. Do not invent webhook URLs, notification recipients, or external delivery secrets. When updating an existing webhook, omit url to keep the stored secret; new webhooks need a user-provided url.",
2021
+ inputSchema: updateContactFormInputSchema.shape,
2022
+ outputSchema: {
2023
+ status: z.enum(["needs_confirmation", "updated"]),
2024
+ projectId: z.string(),
2025
+ impact: z.string().optional(),
2026
+ draft: updateContactFormInputSchema.omit({ confirm: true }).optional(),
2027
+ contactForm: contactFormViewSchema.optional()
2028
+ }
2029
+ },
2030
+ async (input) => {
2031
+ const parsed = updateContactFormInputSchema.parse(input);
2032
+ const auth = await requireCliBaseUrl();
2033
+ const structuredContent = updateContactFormOutputSchema.parse(
2034
+ await performUpdateContactForm(auth, parsed)
2035
+ );
2036
+ return mcpStructured(
2037
+ structuredContent.status === "needs_confirmation" ? "Updating this contact form requires explicit confirmation." : "Updated Suda contact form settings.",
2038
+ structuredContent
2039
+ );
2040
+ }
2041
+ );
1825
2042
  server.registerTool(
1826
2043
  "create_page_draft",
1827
2044
  {