@karixi/payload-ai 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/README.md ADDED
@@ -0,0 +1,163 @@
1
+ # @karixi/payload-ai
2
+
3
+ AI-powered data population and admin features for Payload CMS 3.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add @karixi/payload-ai
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ // payload.config.ts
15
+ import { buildConfig } from 'payload'
16
+ import { mongooseAdapter } from '@payloadcms/db-mongodb'
17
+ import { mcpPlugin } from '@payloadcms/plugin-mcp'
18
+ import { aiPlugin, getAITools, getAIPrompts, getAIResources } from '@karixi/payload-ai'
19
+
20
+ export default buildConfig({
21
+ plugins: [
22
+ aiPlugin({
23
+ provider: 'anthropic',
24
+ apiKeyEnvVar: 'ANTHROPIC_API_KEY',
25
+ features: {
26
+ adminUI: true,
27
+ devTools: false,
28
+ },
29
+ rollbackOnError: true,
30
+ }),
31
+ mcpPlugin({
32
+ tools: (incomingTools) => [...incomingTools, ...getAITools()],
33
+ prompts: (incomingPrompts) => [...incomingPrompts, ...getAIPrompts()],
34
+ resources: (incomingResources) => [...incomingResources, ...getAIResources()],
35
+ }),
36
+ ],
37
+ db: mongooseAdapter({ url: process.env.DATABASE_URI! }),
38
+ collections: [/* your collections */],
39
+ })
40
+ ```
41
+
42
+ ## MCP Tools
43
+
44
+ | Tool | Description |
45
+ |------|-------------|
46
+ | `populateCollection` | Generate N documents for any collection using AI |
47
+ | `bulkPopulate` | Populate the entire site in dependency order |
48
+ | `getCollectionSchema` | View the analyzed schema for a collection |
49
+ | `listCollections` | List all collections with their populatable status |
50
+
51
+ ## Claude Code Integration
52
+
53
+ ```bash
54
+ claude mcp add --transport http Payload http://localhost:3000/api/mcp \
55
+ --header "Authorization: Bearer YOUR_API_KEY"
56
+ ```
57
+
58
+ Then use natural language in Claude Code:
59
+
60
+ ```
61
+ Generate 10 blog posts for the posts collection
62
+ Populate all collections with realistic sample data
63
+ ```
64
+
65
+ ## Features
66
+
67
+ ### Admin UI (`features.adminUI: true`)
68
+
69
+ - **AI Fill** button on individual documents — generates field values with one click
70
+ - **Bulk Generate** panel — populate any collection with N documents from the admin panel
71
+ - **Smart defaults** — AI infers sensible values from field names and collection context
72
+ - **Image alt text** — automatically generates alt text for uploaded media
73
+
74
+ ### Dev Tools (`features.devTools: true`)
75
+
76
+ Requires `@anthropic-ai/stagehand` (optional peer dependency).
77
+
78
+ - `screenshot` — capture screenshots of your frontend
79
+ - `visual_diff` — compare before/after screenshots to catch regressions
80
+ - `test_form` — fill and submit forms programmatically
81
+ - `edit_test_fix` — AI-driven edit → test → fix loop for frontend issues
82
+
83
+ ## Plugin Configuration
84
+
85
+ ```typescript
86
+ aiPlugin({
87
+ provider: 'anthropic' | 'openai', // AI provider
88
+ apiKeyEnvVar: string, // env var name for the API key
89
+ features: {
90
+ adminUI: boolean, // default: false
91
+ devTools: boolean, // default: false
92
+ },
93
+ collections: {
94
+ posts: {
95
+ fields: {
96
+ title: { enabled: true, prompt: 'A compelling blog post title' },
97
+ status: { enabled: false },
98
+ },
99
+ },
100
+ },
101
+ rateLimit: {
102
+ maxTokensPerBatch: 100000,
103
+ delayBetweenRequests: 200, // ms
104
+ maxConcurrentRequests: 3,
105
+ },
106
+ rollbackOnError: true, // delete created docs if bulk run fails
107
+ maxConcurrentBrowserInstances: 2, // for dev tools
108
+ })
109
+ ```
110
+
111
+ ## API
112
+
113
+ ### Plugin entry points
114
+
115
+ ```typescript
116
+ import { aiPlugin } from '@karixi/payload-ai'
117
+ import { getAITools } from '@karixi/payload-ai'
118
+ import { getAIPrompts } from '@karixi/payload-ai'
119
+ import { getAIResources } from '@karixi/payload-ai'
120
+ ```
121
+
122
+ ### Types
123
+
124
+ ```typescript
125
+ import type {
126
+ AIPluginConfig,
127
+ AIProvider,
128
+ CollectionAIConfig,
129
+ CollectionSchema,
130
+ DeletionLogEntry,
131
+ FieldSchema,
132
+ ProgressEvent,
133
+ RelationshipInfo,
134
+ } from '@karixi/payload-ai'
135
+ ```
136
+
137
+ ### Core utilities (advanced)
138
+
139
+ ```typescript
140
+ // Analyze collection fields into a structured schema
141
+ import { analyzeFields } from '@karixi/payload-ai/core/field-analyzer'
142
+
143
+ // Resolve creation order for collections with relationships
144
+ import { resolveCreationOrder, getDependencies } from '@karixi/payload-ai/core/dependency-resolver'
145
+
146
+ // Convert plain text or structured content to Lexical rich text format
147
+ import { textToLexical, contentToLexical } from '@karixi/payload-ai/generate/richtext-generator'
148
+ ```
149
+
150
+ ## Requirements
151
+
152
+ - `payload` ^3.79.0
153
+ - `@payloadcms/plugin-mcp` ^3.79.0
154
+ - Node.js 20+ or Bun 1.1+
155
+ - Optional: `@anthropic-ai/stagehand` (for dev tools feature)
156
+
157
+ ## Development
158
+
159
+ ```bash
160
+ bun run build # compile TypeScript
161
+ bun run typecheck # type-check without emitting
162
+ bun run test # run unit tests
163
+ ```
@@ -0,0 +1,146 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
2
+ //#region src/core/providers/anthropic.ts
3
+ function isAnthropicResponse(value) {
4
+ return typeof value === "object" && value !== null && "content" in value && Array.isArray(value.content);
5
+ }
6
+ function createAnthropicProvider(apiKey) {
7
+ async function callAPI(messages) {
8
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
9
+ method: "POST",
10
+ headers: {
11
+ "x-api-key": apiKey,
12
+ "anthropic-version": "2023-06-01",
13
+ "content-type": "application/json"
14
+ },
15
+ body: JSON.stringify({
16
+ model: "claude-sonnet-4-20250514",
17
+ max_tokens: 8192,
18
+ messages
19
+ })
20
+ });
21
+ if (!response.ok) {
22
+ const errorText = await response.text();
23
+ throw new Error(`Anthropic API error ${response.status}: ${errorText}`);
24
+ }
25
+ const data = await response.json();
26
+ if (!isAnthropicResponse(data)) throw new Error("Unexpected Anthropic API response shape");
27
+ return data;
28
+ }
29
+ return {
30
+ async generate(prompt, _outputSchema) {
31
+ const textBlock = (await callAPI([{
32
+ role: "user",
33
+ content: `${prompt}\n\nRespond with ONLY a valid JSON array. No markdown, no explanation, just the JSON array.`
34
+ }])).content.find((block) => block.type === "text");
35
+ if (!textBlock || !("text" in textBlock) || typeof textBlock.text !== "string") throw new Error("No text content in Anthropic response");
36
+ const jsonText = textBlock.text.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```\s*$/, "").trim();
37
+ const parsed = JSON.parse(jsonText);
38
+ if (!Array.isArray(parsed)) throw new Error("Anthropic response is not a JSON array");
39
+ return parsed;
40
+ },
41
+ async analyzeImage(imageBuffer) {
42
+ const base64 = imageBuffer.toString("base64");
43
+ let mediaType = "image/jpeg";
44
+ if (imageBuffer[0] === 137 && imageBuffer[1] === 80) mediaType = "image/png";
45
+ else if (imageBuffer[0] === 71 && imageBuffer[1] === 73) mediaType = "image/gif";
46
+ else if (imageBuffer[0] === 82 && imageBuffer[1] === 73) mediaType = "image/webp";
47
+ const textBlock = (await callAPI([{
48
+ role: "user",
49
+ content: [{
50
+ type: "image",
51
+ source: {
52
+ type: "base64",
53
+ media_type: mediaType,
54
+ data: base64
55
+ }
56
+ }, {
57
+ type: "text",
58
+ text: "Describe this image concisely for use as alt text. Focus on the main subject and important visual details. Respond with only the alt text description, no extra explanation."
59
+ }]
60
+ }])).content.find((block) => block.type === "text");
61
+ if (!textBlock || !("text" in textBlock) || typeof textBlock.text !== "string") throw new Error("No text content in Anthropic image analysis response");
62
+ return textBlock.text.trim();
63
+ }
64
+ };
65
+ }
66
+ //#endregion
67
+ //#region src/core/providers/openai.ts
68
+ function isOpenAIResponse(value) {
69
+ return typeof value === "object" && value !== null && "choices" in value && Array.isArray(value.choices);
70
+ }
71
+ function createOpenAIProvider(apiKey) {
72
+ async function callAPI(messages, jsonMode) {
73
+ const body = {
74
+ model: "gpt-4o",
75
+ messages
76
+ };
77
+ if (jsonMode) body.response_format = { type: "json_object" };
78
+ const response = await fetch("https://api.openai.com/v1/chat/completions", {
79
+ method: "POST",
80
+ headers: {
81
+ Authorization: `Bearer ${apiKey}`,
82
+ "content-type": "application/json"
83
+ },
84
+ body: JSON.stringify(body)
85
+ });
86
+ if (!response.ok) {
87
+ const errorText = await response.text();
88
+ throw new Error(`OpenAI API error ${response.status}: ${errorText}`);
89
+ }
90
+ const data = await response.json();
91
+ if (!isOpenAIResponse(data)) throw new Error("Unexpected OpenAI API response shape");
92
+ return data;
93
+ }
94
+ return {
95
+ async generate(prompt, _outputSchema) {
96
+ const choice = (await callAPI([{
97
+ role: "system",
98
+ content: "You are a data generation assistant. Always respond with valid JSON only. When asked for an array, wrap it in {\"items\": [...]} so json_object mode is satisfied."
99
+ }, {
100
+ role: "user",
101
+ content: `${prompt}\n\nRespond with JSON object {"items": [...]} where items is the array of generated documents.`
102
+ }], true)).choices[0];
103
+ if (!choice || choice.message.content === null) throw new Error("No content in OpenAI response");
104
+ const parsed = JSON.parse(choice.message.content);
105
+ if (typeof parsed === "object" && parsed !== null && "items" in parsed && Array.isArray(parsed.items)) return parsed.items;
106
+ if (Array.isArray(parsed)) return parsed;
107
+ throw new Error("OpenAI response is not a JSON array");
108
+ },
109
+ async analyzeImage(imageBuffer) {
110
+ const base64 = imageBuffer.toString("base64");
111
+ let mediaType = "image/jpeg";
112
+ if (imageBuffer[0] === 137 && imageBuffer[1] === 80) mediaType = "image/png";
113
+ else if (imageBuffer[0] === 71 && imageBuffer[1] === 73) mediaType = "image/gif";
114
+ else if (imageBuffer[0] === 82 && imageBuffer[1] === 73) mediaType = "image/webp";
115
+ const choice = (await callAPI([{
116
+ role: "user",
117
+ content: [{
118
+ type: "image_url",
119
+ image_url: { url: `data:${mediaType};base64,${base64}` }
120
+ }, {
121
+ type: "text",
122
+ text: "Describe this image concisely for use as alt text. Focus on the main subject and important visual details. Respond with only the alt text description, no extra explanation."
123
+ }]
124
+ }], false)).choices[0];
125
+ if (!choice || choice.message.content === null) throw new Error("No content in OpenAI image analysis response");
126
+ return choice.message.content.trim();
127
+ }
128
+ };
129
+ }
130
+ //#endregion
131
+ //#region src/core/providers/base.ts
132
+ var base_exports = /* @__PURE__ */ __exportAll({ createProvider: () => createProvider });
133
+ function createProvider(config) {
134
+ switch (config.provider) {
135
+ case "anthropic": return createAnthropicProvider(config.apiKey);
136
+ case "openai": return createOpenAIProvider(config.apiKey);
137
+ default: {
138
+ const _exhaustive = config.provider;
139
+ throw new Error(`Unknown provider: ${String(_exhaustive)}`);
140
+ }
141
+ }
142
+ }
143
+ //#endregion
144
+ export { createProvider as n, base_exports as t };
145
+
146
+ //# sourceMappingURL=base-BPwZbeh1.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-BPwZbeh1.mjs","names":[],"sources":["../src/core/providers/anthropic.ts","../src/core/providers/openai.ts","../src/core/providers/base.ts"],"sourcesContent":["import type { AIProvider } from '../../types.js'\n\ntype AnthropicMessage = {\n role: 'user' | 'assistant'\n content: string | AnthropicContentBlock[]\n}\n\ntype AnthropicContentBlock =\n | { type: 'text'; text: string }\n | { type: 'image'; source: { type: 'base64'; media_type: string; data: string } }\n\ntype AnthropicResponse = {\n content: Array<{ type: string; text?: string }>\n usage?: { input_tokens: number; output_tokens: number }\n}\n\nfunction isAnthropicResponse(value: unknown): value is AnthropicResponse {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'content' in value &&\n Array.isArray((value as AnthropicResponse).content)\n )\n}\n\nexport function createAnthropicProvider(apiKey: string): AIProvider {\n async function callAPI(messages: AnthropicMessage[]): Promise<AnthropicResponse> {\n const response = await fetch('https://api.anthropic.com/v1/messages', {\n method: 'POST',\n headers: {\n 'x-api-key': apiKey,\n 'anthropic-version': '2023-06-01',\n 'content-type': 'application/json',\n },\n body: JSON.stringify({\n model: 'claude-sonnet-4-20250514',\n max_tokens: 8192,\n messages,\n }),\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new Error(`Anthropic API error ${response.status}: ${errorText}`)\n }\n\n const data: unknown = await response.json()\n if (!isAnthropicResponse(data)) {\n throw new Error('Unexpected Anthropic API response shape')\n }\n return data\n }\n\n return {\n async generate(prompt: string, _outputSchema: Record<string, unknown>): Promise<unknown[]> {\n const messages: AnthropicMessage[] = [\n {\n role: 'user',\n content: `${prompt}\\n\\nRespond with ONLY a valid JSON array. No markdown, no explanation, just the JSON array.`,\n },\n ]\n\n const data = await callAPI(messages)\n const textBlock = data.content.find((block) => block.type === 'text')\n if (!textBlock || !('text' in textBlock) || typeof textBlock.text !== 'string') {\n throw new Error('No text content in Anthropic response')\n }\n\n const text = textBlock.text.trim()\n // Strip markdown code fences if present\n const jsonText = text\n .replace(/^```(?:json)?\\s*/i, '')\n .replace(/\\s*```\\s*$/, '')\n .trim()\n\n const parsed: unknown = JSON.parse(jsonText)\n if (!Array.isArray(parsed)) {\n throw new Error('Anthropic response is not a JSON array')\n }\n return parsed\n },\n\n async analyzeImage(imageBuffer: Buffer): Promise<string> {\n const base64 = imageBuffer.toString('base64')\n // Detect image type from buffer magic bytes\n let mediaType = 'image/jpeg'\n if (imageBuffer[0] === 0x89 && imageBuffer[1] === 0x50) mediaType = 'image/png'\n else if (imageBuffer[0] === 0x47 && imageBuffer[1] === 0x49) mediaType = 'image/gif'\n else if (imageBuffer[0] === 0x52 && imageBuffer[1] === 0x49) mediaType = 'image/webp'\n\n const messages: AnthropicMessage[] = [\n {\n role: 'user',\n content: [\n {\n type: 'image',\n source: { type: 'base64', media_type: mediaType, data: base64 },\n },\n {\n type: 'text',\n text: 'Describe this image concisely for use as alt text. Focus on the main subject and important visual details. Respond with only the alt text description, no extra explanation.',\n },\n ],\n },\n ]\n\n const data = await callAPI(messages)\n const textBlock = data.content.find((block) => block.type === 'text')\n if (!textBlock || !('text' in textBlock) || typeof textBlock.text !== 'string') {\n throw new Error('No text content in Anthropic image analysis response')\n }\n return textBlock.text.trim()\n },\n }\n}\n","import type { AIProvider } from '../../types.js'\n\ntype OpenAIMessage = {\n role: 'user' | 'assistant' | 'system'\n content: string | OpenAIContentBlock[]\n}\n\ntype OpenAIContentBlock =\n | { type: 'text'; text: string }\n | { type: 'image_url'; image_url: { url: string } }\n\ntype OpenAIResponse = {\n choices: Array<{\n message: { role: string; content: string | null }\n }>\n usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number }\n}\n\nfunction isOpenAIResponse(value: unknown): value is OpenAIResponse {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'choices' in value &&\n Array.isArray((value as OpenAIResponse).choices)\n )\n}\n\nexport function createOpenAIProvider(apiKey: string): AIProvider {\n async function callAPI(messages: OpenAIMessage[], jsonMode: boolean): Promise<OpenAIResponse> {\n const body: Record<string, unknown> = {\n model: 'gpt-4o',\n messages,\n }\n if (jsonMode) {\n body.response_format = { type: 'json_object' }\n }\n\n const response = await fetch('https://api.openai.com/v1/chat/completions', {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'content-type': 'application/json',\n },\n body: JSON.stringify(body),\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new Error(`OpenAI API error ${response.status}: ${errorText}`)\n }\n\n const data: unknown = await response.json()\n if (!isOpenAIResponse(data)) {\n throw new Error('Unexpected OpenAI API response shape')\n }\n return data\n }\n\n return {\n async generate(prompt: string, _outputSchema: Record<string, unknown>): Promise<unknown[]> {\n const messages: OpenAIMessage[] = [\n {\n role: 'system',\n content:\n 'You are a data generation assistant. Always respond with valid JSON only. When asked for an array, wrap it in {\"items\": [...]} so json_object mode is satisfied.',\n },\n {\n role: 'user',\n content: `${prompt}\\n\\nRespond with JSON object {\"items\": [...]} where items is the array of generated documents.`,\n },\n ]\n\n const data = await callAPI(messages, true)\n const choice = data.choices[0]\n if (!choice || choice.message.content === null) {\n throw new Error('No content in OpenAI response')\n }\n\n const parsed: unknown = JSON.parse(choice.message.content)\n if (\n typeof parsed === 'object' &&\n parsed !== null &&\n 'items' in parsed &&\n Array.isArray((parsed as { items: unknown }).items)\n ) {\n return (parsed as { items: unknown[] }).items\n }\n if (Array.isArray(parsed)) {\n return parsed\n }\n throw new Error('OpenAI response is not a JSON array')\n },\n\n async analyzeImage(imageBuffer: Buffer): Promise<string> {\n const base64 = imageBuffer.toString('base64')\n // Detect image type from buffer magic bytes\n let mediaType = 'image/jpeg'\n if (imageBuffer[0] === 0x89 && imageBuffer[1] === 0x50) mediaType = 'image/png'\n else if (imageBuffer[0] === 0x47 && imageBuffer[1] === 0x49) mediaType = 'image/gif'\n else if (imageBuffer[0] === 0x52 && imageBuffer[1] === 0x49) mediaType = 'image/webp'\n\n const dataUrl = `data:${mediaType};base64,${base64}`\n\n const messages: OpenAIMessage[] = [\n {\n role: 'user',\n content: [\n {\n type: 'image_url',\n image_url: { url: dataUrl },\n },\n {\n type: 'text',\n text: 'Describe this image concisely for use as alt text. Focus on the main subject and important visual details. Respond with only the alt text description, no extra explanation.',\n },\n ],\n },\n ]\n\n const data = await callAPI(messages, false)\n const choice = data.choices[0]\n if (!choice || choice.message.content === null) {\n throw new Error('No content in OpenAI image analysis response')\n }\n return choice.message.content.trim()\n },\n }\n}\n","import type { AIProvider } from '../../types.js'\nimport { createAnthropicProvider } from './anthropic.js'\nimport { createOpenAIProvider } from './openai.js'\n\nexport type { AIProvider }\n\nexport type ProviderConfig = {\n provider: 'anthropic' | 'openai'\n apiKey: string\n}\n\nexport function createProvider(config: ProviderConfig): AIProvider {\n switch (config.provider) {\n case 'anthropic':\n return createAnthropicProvider(config.apiKey)\n case 'openai':\n return createOpenAIProvider(config.apiKey)\n default: {\n const _exhaustive: never = config.provider\n throw new Error(`Unknown provider: ${String(_exhaustive)}`)\n }\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,220 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-wcPFST8Q.mjs";
2
+ //#region src/core/prompt-builder.ts
3
+ function describeField(field, existingIds) {
4
+ const lines = [];
5
+ const required = field.required ? " (required)" : " (optional)";
6
+ switch (field.type) {
7
+ case "text":
8
+ case "textarea":
9
+ case "email":
10
+ lines.push(`- "${field.name}" (${field.type}${required}): Generate realistic ${field.type} content`);
11
+ break;
12
+ case "number":
13
+ lines.push(`- "${field.name}" (number${required}): Generate a realistic numeric value (e.g. price 1–999, quantity 1–100, rating 1–5)`);
14
+ break;
15
+ case "checkbox":
16
+ lines.push(`- "${field.name}" (boolean${required}): true or false`);
17
+ break;
18
+ case "date":
19
+ lines.push(`- "${field.name}" (date${required}): ISO 8601 date string (e.g. "2024-06-15T10:00:00.000Z")`);
20
+ break;
21
+ case "select": {
22
+ const values = (field.options ?? []).map((o) => `"${o.value}"`).join(", ");
23
+ lines.push(`- "${field.name}" (select${required}): Must be one of [${values}]`);
24
+ break;
25
+ }
26
+ case "relationship": {
27
+ const collections = Array.isArray(field.relationTo) ? field.relationTo : [field.relationTo ?? ""];
28
+ const idLists = collections.map((col) => {
29
+ const ids = existingIds?.[col] ?? [];
30
+ return ids.length > 0 ? `${col}: [${ids.map((id) => `"${id}"`).join(", ")}]` : `${col}: (no existing IDs available — omit this field)`;
31
+ });
32
+ if (collections.some((col) => (existingIds?.[col] ?? []).length > 0)) lines.push(`- "${field.name}" (relationship${required}): Pick from existing IDs — ${idLists.join("; ")}${field.hasMany ? " (can be an array of IDs)" : " (single ID string)"}`);
33
+ else lines.push(`- "${field.name}" (relationship${required}): SKIP — no existing IDs available`);
34
+ break;
35
+ }
36
+ case "richText":
37
+ lines.push(`- "${field.name}" (richtext${required}): Return PLAIN TEXT only (do not wrap in Lexical/JSON — the system will convert it). Write 1–3 sentences of realistic content.`);
38
+ break;
39
+ case "upload":
40
+ lines.push(`- "${field.name}" (upload${required}): SKIP — handled separately`);
41
+ break;
42
+ case "array":
43
+ case "group":
44
+ if (field.fields && field.fields.length > 0) {
45
+ lines.push(`- "${field.name}" (${field.type}${required}):`);
46
+ for (const subField of field.fields) lines.push(` ${describeField(subField, existingIds)}`);
47
+ }
48
+ break;
49
+ case "blocks":
50
+ lines.push(`- "${field.name}" (blocks${required}): SKIP — complex layout field, omit`);
51
+ break;
52
+ default: lines.push(`- "${field.name}" (${field.type}${required}): Generate appropriate content`);
53
+ }
54
+ return lines.join("\n");
55
+ }
56
+ function buildGenerationPrompt(schema, context) {
57
+ const fieldDescriptions = schema.fields.map((f) => describeField(f, context.existingIds)).join("\n");
58
+ const requiredNote = schema.requiredFields.length > 0 ? `\nRequired fields (must be present): ${schema.requiredFields.map((f) => `"${f}"`).join(", ")}` : "";
59
+ const themeNote = context.theme ? `\nContent theme/style: ${context.theme}` : "";
60
+ const localeNote = context.locale && context.locale !== "en" ? `\nGenerate content in locale: ${context.locale}` : "";
61
+ return `Generate ${context.count} realistic document(s) for the "${schema.slug}" collection.
62
+ ${themeNote}${localeNote}${requiredNote}
63
+
64
+ Fields to generate:
65
+ ${fieldDescriptions}
66
+
67
+ Rules:
68
+ - Return a JSON array with exactly ${context.count} item(s)
69
+ - Each item must be a flat JSON object with field names as keys
70
+ - Skip fields marked as SKIP
71
+ - For richtext fields: return plain text strings only
72
+ - For relationship fields: use the provided existing IDs exactly as shown
73
+ - Do not include extra fields not listed above
74
+ - Generate varied, realistic content appropriate for an ecommerce platform`;
75
+ }
76
+ function buildOutputSchema(schema) {
77
+ const properties = {};
78
+ const required = [];
79
+ for (const field of schema.fields) {
80
+ if ([
81
+ "relationship",
82
+ "richText",
83
+ "upload",
84
+ "blocks"
85
+ ].includes(field.type)) continue;
86
+ let fieldSchema;
87
+ switch (field.type) {
88
+ case "text":
89
+ case "textarea":
90
+ case "email":
91
+ case "date":
92
+ fieldSchema = { type: "string" };
93
+ break;
94
+ case "number":
95
+ fieldSchema = { type: "number" };
96
+ break;
97
+ case "checkbox":
98
+ fieldSchema = { type: "boolean" };
99
+ break;
100
+ case "select":
101
+ fieldSchema = {
102
+ type: "string",
103
+ enum: (field.options ?? []).map((o) => o.value)
104
+ };
105
+ break;
106
+ case "array":
107
+ fieldSchema = {
108
+ type: "array",
109
+ items: { type: "object" }
110
+ };
111
+ break;
112
+ case "group":
113
+ fieldSchema = { type: "object" };
114
+ break;
115
+ default: fieldSchema = { type: "string" };
116
+ }
117
+ properties[field.name] = fieldSchema;
118
+ if (field.required) required.push(field.name);
119
+ }
120
+ return {
121
+ type: "object",
122
+ properties: { items: {
123
+ type: "array",
124
+ items: {
125
+ type: "object",
126
+ properties,
127
+ required
128
+ }
129
+ } }
130
+ };
131
+ }
132
+ //#endregion
133
+ //#region src/core/content-generator.ts
134
+ var content_generator_exports = /* @__PURE__ */ __exportAll({ generateDocuments: () => generateDocuments });
135
+ function getSelectValues(field) {
136
+ return (field.options ?? []).map((o) => o.value);
137
+ }
138
+ function validateDocument(doc, schema) {
139
+ const errors = [];
140
+ if (typeof doc !== "object" || doc === null || Array.isArray(doc)) return [{
141
+ field: "_root",
142
+ message: "Document must be a plain object"
143
+ }];
144
+ const record = doc;
145
+ for (const fieldName of schema.requiredFields) {
146
+ const field = schema.fields.find((f) => f.name === fieldName);
147
+ if (!field) continue;
148
+ if ([
149
+ "relationship",
150
+ "richText",
151
+ "upload",
152
+ "blocks"
153
+ ].includes(field.type)) continue;
154
+ if (record[fieldName] === void 0 || record[fieldName] === null || record[fieldName] === "") errors.push({
155
+ field: fieldName,
156
+ message: `Required field "${fieldName}" is missing or empty`
157
+ });
158
+ }
159
+ for (const field of schema.fields) {
160
+ if (field.type !== "select") continue;
161
+ const value = record[field.name];
162
+ if (value === void 0 || value === null) continue;
163
+ const validValues = getSelectValues(field);
164
+ if (validValues.length > 0 && !validValues.includes(String(value))) errors.push({
165
+ field: field.name,
166
+ message: `Field "${field.name}" has invalid select value "${String(value)}". Must be one of: ${validValues.join(", ")}`
167
+ });
168
+ }
169
+ return errors;
170
+ }
171
+ function buildRetryPrompt(originalPrompt, errors) {
172
+ return `${originalPrompt}
173
+
174
+ IMPORTANT: Your previous response had validation errors. Fix these issues:
175
+ ${errors.map((e) => `- ${e.field}: ${e.message}`).join("\n")}
176
+
177
+ Return a corrected JSON array addressing all the errors above.`;
178
+ }
179
+ async function generateDocuments(provider, schema, context, options) {
180
+ const maxRetries = options?.maxRetries ?? 3;
181
+ const outputSchema = buildOutputSchema(schema);
182
+ let prompt = buildGenerationPrompt(schema, context);
183
+ let lastError = null;
184
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
185
+ let rawItems;
186
+ try {
187
+ rawItems = await provider.generate(prompt, outputSchema);
188
+ } catch (err) {
189
+ lastError = err instanceof Error ? err : new Error(String(err));
190
+ if (attempt < maxRetries) {
191
+ prompt = buildRetryPrompt(buildGenerationPrompt(schema, context), [{
192
+ field: "_generation",
193
+ message: `API error: ${lastError.message}`
194
+ }]);
195
+ continue;
196
+ }
197
+ throw lastError;
198
+ }
199
+ const allErrors = [];
200
+ const validDocuments = [];
201
+ for (const item of rawItems) {
202
+ const errors = validateDocument(item, schema);
203
+ if (errors.length > 0) allErrors.push(...errors);
204
+ else validDocuments.push(item);
205
+ }
206
+ if (allErrors.length === 0) return { documents: validDocuments };
207
+ if (attempt < maxRetries) {
208
+ prompt = buildRetryPrompt(buildGenerationPrompt(schema, context), allErrors);
209
+ lastError = /* @__PURE__ */ new Error(`Validation failed: ${allErrors.map((e) => e.message).join("; ")}`);
210
+ continue;
211
+ }
212
+ if (validDocuments.length > 0) return { documents: validDocuments };
213
+ throw new Error(`Content generation failed after ${maxRetries} retries. Last errors: ${allErrors.map((e) => e.message).join("; ")}`);
214
+ }
215
+ throw lastError ?? /* @__PURE__ */ new Error("Content generation failed");
216
+ }
217
+ //#endregion
218
+ export { generateDocuments as n, content_generator_exports as t };
219
+
220
+ //# sourceMappingURL=content-generator-fPX2DL3g.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-generator-fPX2DL3g.mjs","names":[],"sources":["../src/core/prompt-builder.ts","../src/core/content-generator.ts"],"sourcesContent":["import type { CollectionSchema, FieldSchema } from '../types.js'\n\nexport type GenerationContext = {\n count: number\n theme?: string\n locale?: string\n existingIds?: Record<string, string[]>\n}\n\nfunction describeField(field: FieldSchema, existingIds?: Record<string, string[]>): string {\n const lines: string[] = []\n const required = field.required ? ' (required)' : ' (optional)'\n\n switch (field.type) {\n case 'text':\n case 'textarea':\n case 'email':\n lines.push(\n `- \"${field.name}\" (${field.type}${required}): Generate realistic ${field.type} content`,\n )\n break\n\n case 'number':\n lines.push(\n `- \"${field.name}\" (number${required}): Generate a realistic numeric value (e.g. price 1–999, quantity 1–100, rating 1–5)`,\n )\n break\n\n case 'checkbox':\n lines.push(`- \"${field.name}\" (boolean${required}): true or false`)\n break\n\n case 'date':\n lines.push(\n `- \"${field.name}\" (date${required}): ISO 8601 date string (e.g. \"2024-06-15T10:00:00.000Z\")`,\n )\n break\n\n case 'select': {\n const values = (field.options ?? []).map((o) => `\"${o.value}\"`).join(', ')\n lines.push(`- \"${field.name}\" (select${required}): Must be one of [${values}]`)\n break\n }\n\n case 'relationship': {\n const collections = Array.isArray(field.relationTo)\n ? field.relationTo\n : [field.relationTo ?? '']\n const idLists = collections.map((col) => {\n const ids = existingIds?.[col] ?? []\n return ids.length > 0\n ? `${col}: [${ids.map((id) => `\"${id}\"`).join(', ')}]`\n : `${col}: (no existing IDs available — omit this field)`\n })\n const hasIds = collections.some((col) => (existingIds?.[col] ?? []).length > 0)\n if (hasIds) {\n lines.push(\n `- \"${field.name}\" (relationship${required}): Pick from existing IDs — ${idLists.join('; ')}${field.hasMany ? ' (can be an array of IDs)' : ' (single ID string)'}`,\n )\n } else {\n lines.push(`- \"${field.name}\" (relationship${required}): SKIP — no existing IDs available`)\n }\n break\n }\n\n case 'richText':\n lines.push(\n `- \"${field.name}\" (richtext${required}): Return PLAIN TEXT only (do not wrap in Lexical/JSON — the system will convert it). Write 1–3 sentences of realistic content.`,\n )\n break\n\n case 'upload':\n lines.push(`- \"${field.name}\" (upload${required}): SKIP — handled separately`)\n break\n\n case 'array':\n case 'group':\n if (field.fields && field.fields.length > 0) {\n lines.push(`- \"${field.name}\" (${field.type}${required}):`)\n for (const subField of field.fields) {\n lines.push(` ${describeField(subField, existingIds)}`)\n }\n }\n break\n\n case 'blocks':\n lines.push(`- \"${field.name}\" (blocks${required}): SKIP — complex layout field, omit`)\n break\n\n default:\n lines.push(`- \"${field.name}\" (${field.type}${required}): Generate appropriate content`)\n }\n\n return lines.join('\\n')\n}\n\nexport function buildGenerationPrompt(\n schema: CollectionSchema,\n context: GenerationContext,\n): string {\n const fieldDescriptions = schema.fields\n .map((f) => describeField(f, context.existingIds))\n .join('\\n')\n\n const requiredNote =\n schema.requiredFields.length > 0\n ? `\\nRequired fields (must be present): ${schema.requiredFields.map((f) => `\"${f}\"`).join(', ')}`\n : ''\n\n const themeNote = context.theme ? `\\nContent theme/style: ${context.theme}` : ''\n\n const localeNote =\n context.locale && context.locale !== 'en'\n ? `\\nGenerate content in locale: ${context.locale}`\n : ''\n\n return `Generate ${context.count} realistic document(s) for the \"${schema.slug}\" collection.\n${themeNote}${localeNote}${requiredNote}\n\nFields to generate:\n${fieldDescriptions}\n\nRules:\n- Return a JSON array with exactly ${context.count} item(s)\n- Each item must be a flat JSON object with field names as keys\n- Skip fields marked as SKIP\n- For richtext fields: return plain text strings only\n- For relationship fields: use the provided existing IDs exactly as shown\n- Do not include extra fields not listed above\n- Generate varied, realistic content appropriate for an ecommerce platform`\n}\n\nexport function buildOutputSchema(schema: CollectionSchema): Record<string, unknown> {\n const properties: Record<string, unknown> = {}\n const required: string[] = []\n\n for (const field of schema.fields) {\n // Skip fields handled separately\n if (['relationship', 'richText', 'upload', 'blocks'].includes(field.type)) {\n continue\n }\n\n let fieldSchema: Record<string, unknown>\n\n switch (field.type) {\n case 'text':\n case 'textarea':\n case 'email':\n case 'date':\n fieldSchema = { type: 'string' }\n break\n\n case 'number':\n fieldSchema = { type: 'number' }\n break\n\n case 'checkbox':\n fieldSchema = { type: 'boolean' }\n break\n\n case 'select': {\n const enumValues = (field.options ?? []).map((o) => o.value)\n fieldSchema = { type: 'string', enum: enumValues }\n break\n }\n\n case 'array':\n fieldSchema = { type: 'array', items: { type: 'object' } }\n break\n\n case 'group':\n fieldSchema = { type: 'object' }\n break\n\n default:\n fieldSchema = { type: 'string' }\n }\n\n properties[field.name] = fieldSchema\n if (field.required) {\n required.push(field.name)\n }\n }\n\n return {\n type: 'object',\n properties: {\n items: {\n type: 'array',\n items: {\n type: 'object',\n properties,\n required,\n },\n },\n },\n }\n}\n","import type { AIProvider, CollectionSchema, FieldSchema } from '../types.js'\nimport type { GenerationContext } from './prompt-builder.js'\nimport { buildGenerationPrompt, buildOutputSchema } from './prompt-builder.js'\n\nexport type { GenerationContext }\n\nexport type GenerationResult = {\n documents: Record<string, unknown>[]\n tokensUsed?: number\n}\n\ntype ValidationError = {\n field: string\n message: string\n}\n\nfunction getSelectValues(field: FieldSchema): string[] {\n return (field.options ?? []).map((o) => o.value)\n}\n\nfunction validateDocument(doc: unknown, schema: CollectionSchema): ValidationError[] {\n const errors: ValidationError[] = []\n\n if (typeof doc !== 'object' || doc === null || Array.isArray(doc)) {\n return [{ field: '_root', message: 'Document must be a plain object' }]\n }\n\n const record = doc as Record<string, unknown>\n\n // Check required fields\n for (const fieldName of schema.requiredFields) {\n const field = schema.fields.find((f) => f.name === fieldName)\n if (!field) continue\n // Skip fields that are handled separately\n if (['relationship', 'richText', 'upload', 'blocks'].includes(field.type)) continue\n if (record[fieldName] === undefined || record[fieldName] === null || record[fieldName] === '') {\n errors.push({\n field: fieldName,\n message: `Required field \"${fieldName}\" is missing or empty`,\n })\n }\n }\n\n // Validate select field values\n for (const field of schema.fields) {\n if (field.type !== 'select') continue\n const value = record[field.name]\n if (value === undefined || value === null) continue\n const validValues = getSelectValues(field)\n if (validValues.length > 0 && !validValues.includes(String(value))) {\n errors.push({\n field: field.name,\n message: `Field \"${field.name}\" has invalid select value \"${String(value)}\". Must be one of: ${validValues.join(', ')}`,\n })\n }\n }\n\n return errors\n}\n\nfunction buildRetryPrompt(originalPrompt: string, errors: ValidationError[]): string {\n const errorList = errors.map((e) => `- ${e.field}: ${e.message}`).join('\\n')\n return `${originalPrompt}\n\nIMPORTANT: Your previous response had validation errors. Fix these issues:\n${errorList}\n\nReturn a corrected JSON array addressing all the errors above.`\n}\n\nexport async function generateDocuments(\n provider: AIProvider,\n schema: CollectionSchema,\n context: GenerationContext,\n options?: { maxRetries?: number },\n): Promise<GenerationResult> {\n const maxRetries = options?.maxRetries ?? 3\n const outputSchema = buildOutputSchema(schema)\n\n let prompt = buildGenerationPrompt(schema, context)\n let lastError: Error | null = null\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n let rawItems: unknown[]\n\n try {\n rawItems = await provider.generate(prompt, outputSchema)\n } catch (err) {\n lastError = err instanceof Error ? err : new Error(String(err))\n if (attempt < maxRetries) {\n prompt = buildRetryPrompt(buildGenerationPrompt(schema, context), [\n { field: '_generation', message: `API error: ${lastError.message}` },\n ])\n continue\n }\n throw lastError\n }\n\n // Validate all documents\n const allErrors: ValidationError[] = []\n const validDocuments: Record<string, unknown>[] = []\n\n for (const item of rawItems) {\n const errors = validateDocument(item, schema)\n if (errors.length > 0) {\n allErrors.push(...errors)\n } else {\n validDocuments.push(item as Record<string, unknown>)\n }\n }\n\n if (allErrors.length === 0) {\n return { documents: validDocuments }\n }\n\n // Validation failed — retry if attempts remain\n if (attempt < maxRetries) {\n prompt = buildRetryPrompt(buildGenerationPrompt(schema, context), allErrors)\n lastError = new Error(`Validation failed: ${allErrors.map((e) => e.message).join('; ')}`)\n continue\n }\n\n // Out of retries — return what we have if any valid docs, otherwise throw\n if (validDocuments.length > 0) {\n return { documents: validDocuments }\n }\n\n throw new Error(\n `Content generation failed after ${maxRetries} retries. Last errors: ${allErrors.map((e) => e.message).join('; ')}`,\n )\n }\n\n throw lastError ?? new Error('Content generation failed')\n}\n"],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ //#region src/core/dependency-resolver.ts
2
+ /**
3
+ * Returns the direct dependency slugs for a collection schema.
4
+ * Dependencies are collections that must be created before this one.
5
+ * Self-referential relationships are excluded (handled via deferred updates).
6
+ */
7
+ function getDependencies(schema) {
8
+ const deps = /* @__PURE__ */ new Set();
9
+ for (const rel of schema.relationships) {
10
+ if (rel.isSelfReferential) continue;
11
+ const targets = Array.isArray(rel.collection) ? rel.collection : [rel.collection];
12
+ for (const target of targets) if (target !== schema.slug) deps.add(target);
13
+ }
14
+ return [...deps];
15
+ }
16
+ /**
17
+ * Topologically sorts collection schemas using Kahn's algorithm.
18
+ * Returns slugs in creation order (dependencies first).
19
+ * Circular dependencies (excluding self-referential) are broken arbitrarily
20
+ * and a warning is emitted.
21
+ */
22
+ function resolveCreationOrder(schemas) {
23
+ const slugSet = new Set(schemas.map((s) => s.slug));
24
+ const deps = /* @__PURE__ */ new Map();
25
+ const dependents = /* @__PURE__ */ new Map();
26
+ for (const schema of schemas) {
27
+ if (!deps.has(schema.slug)) deps.set(schema.slug, /* @__PURE__ */ new Set());
28
+ if (!dependents.has(schema.slug)) dependents.set(schema.slug, /* @__PURE__ */ new Set());
29
+ for (const dep of getDependencies(schema)) {
30
+ if (!slugSet.has(dep)) continue;
31
+ deps.get(schema.slug)?.add(dep);
32
+ if (!dependents.has(dep)) dependents.set(dep, /* @__PURE__ */ new Set());
33
+ dependents.get(dep)?.add(schema.slug);
34
+ }
35
+ }
36
+ const inDegree = /* @__PURE__ */ new Map();
37
+ for (const schema of schemas) inDegree.set(schema.slug, deps.get(schema.slug)?.size ?? 0);
38
+ const queue = [];
39
+ for (const [slug, degree] of inDegree) if (degree === 0) queue.push(slug);
40
+ queue.sort();
41
+ const order = [];
42
+ while (queue.length > 0) {
43
+ queue.sort();
44
+ const slug = queue.shift();
45
+ order.push(slug);
46
+ for (const dependent of dependents.get(slug) ?? []) {
47
+ const newDegree = (inDegree.get(dependent) ?? 1) - 1;
48
+ inDegree.set(dependent, newDegree);
49
+ if (newDegree === 0) queue.push(dependent);
50
+ }
51
+ }
52
+ if (order.length < schemas.length) {
53
+ const remaining = schemas.map((s) => s.slug).filter((s) => !order.includes(s));
54
+ console.warn(`[dependency-resolver] Circular dependency detected among: ${remaining.join(", ")}. These will be appended in arbitrary order.`);
55
+ order.push(...remaining.sort());
56
+ }
57
+ return order;
58
+ }
59
+ //#endregion
60
+ export { resolveCreationOrder };
61
+
62
+ //# sourceMappingURL=dependency-resolver-BgsUvfRJ.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dependency-resolver-BgsUvfRJ.mjs","names":[],"sources":["../src/core/dependency-resolver.ts"],"sourcesContent":["import type { CollectionSchema } from '../types.js'\n\n/**\n * Returns the direct dependency slugs for a collection schema.\n * Dependencies are collections that must be created before this one.\n * Self-referential relationships are excluded (handled via deferred updates).\n */\nexport function getDependencies(schema: CollectionSchema): string[] {\n const deps = new Set<string>()\n\n for (const rel of schema.relationships) {\n if (rel.isSelfReferential) continue\n\n const targets = Array.isArray(rel.collection) ? rel.collection : [rel.collection]\n for (const target of targets) {\n if (target !== schema.slug) {\n deps.add(target)\n }\n }\n }\n\n return [...deps]\n}\n\n/**\n * Topologically sorts collection schemas using Kahn's algorithm.\n * Returns slugs in creation order (dependencies first).\n * Circular dependencies (excluding self-referential) are broken arbitrarily\n * and a warning is emitted.\n */\nexport function resolveCreationOrder(schemas: CollectionSchema[]): string[] {\n const slugSet = new Set(schemas.map((s) => s.slug))\n\n // Build adjacency list: slug → set of slugs it depends on (that are in our schema set)\n const deps = new Map<string, Set<string>>()\n // Reverse: slug → set of slugs that depend on it\n const dependents = new Map<string, Set<string>>()\n\n for (const schema of schemas) {\n if (!deps.has(schema.slug)) deps.set(schema.slug, new Set())\n if (!dependents.has(schema.slug)) dependents.set(schema.slug, new Set())\n\n for (const dep of getDependencies(schema)) {\n // Only track dependencies within the provided schema set\n if (!slugSet.has(dep)) continue\n deps.get(schema.slug)?.add(dep)\n\n if (!dependents.has(dep)) dependents.set(dep, new Set())\n dependents.get(dep)?.add(schema.slug)\n }\n }\n\n // Kahn's algorithm\n const inDegree = new Map<string, number>()\n for (const schema of schemas) {\n inDegree.set(schema.slug, deps.get(schema.slug)?.size ?? 0)\n }\n\n const queue: string[] = []\n for (const [slug, degree] of inDegree) {\n if (degree === 0) queue.push(slug)\n }\n // Sort for deterministic output\n queue.sort()\n\n const order: string[] = []\n\n while (queue.length > 0) {\n // Sort queue for determinism each iteration\n queue.sort()\n const slug = queue.shift()!\n order.push(slug)\n\n for (const dependent of dependents.get(slug) ?? []) {\n const newDegree = (inDegree.get(dependent) ?? 1) - 1\n inDegree.set(dependent, newDegree)\n if (newDegree === 0) {\n queue.push(dependent)\n }\n }\n }\n\n // If we didn't include everything, there are circular dependencies\n if (order.length < schemas.length) {\n const remaining = schemas.map((s) => s.slug).filter((s) => !order.includes(s))\n console.warn(\n `[dependency-resolver] Circular dependency detected among: ${remaining.join(', ')}. ` +\n 'These will be appended in arbitrary order.',\n )\n order.push(...remaining.sort())\n }\n\n return order\n}\n"],"mappings":";;;;;;AAOA,SAAgB,gBAAgB,QAAoC;CAClE,MAAM,uBAAO,IAAI,KAAa;AAE9B,MAAK,MAAM,OAAO,OAAO,eAAe;AACtC,MAAI,IAAI,kBAAmB;EAE3B,MAAM,UAAU,MAAM,QAAQ,IAAI,WAAW,GAAG,IAAI,aAAa,CAAC,IAAI,WAAW;AACjF,OAAK,MAAM,UAAU,QACnB,KAAI,WAAW,OAAO,KACpB,MAAK,IAAI,OAAO;;AAKtB,QAAO,CAAC,GAAG,KAAK;;;;;;;;AASlB,SAAgB,qBAAqB,SAAuC;CAC1E,MAAM,UAAU,IAAI,IAAI,QAAQ,KAAK,MAAM,EAAE,KAAK,CAAC;CAGnD,MAAM,uBAAO,IAAI,KAA0B;CAE3C,MAAM,6BAAa,IAAI,KAA0B;AAEjD,MAAK,MAAM,UAAU,SAAS;AAC5B,MAAI,CAAC,KAAK,IAAI,OAAO,KAAK,CAAE,MAAK,IAAI,OAAO,sBAAM,IAAI,KAAK,CAAC;AAC5D,MAAI,CAAC,WAAW,IAAI,OAAO,KAAK,CAAE,YAAW,IAAI,OAAO,sBAAM,IAAI,KAAK,CAAC;AAExE,OAAK,MAAM,OAAO,gBAAgB,OAAO,EAAE;AAEzC,OAAI,CAAC,QAAQ,IAAI,IAAI,CAAE;AACvB,QAAK,IAAI,OAAO,KAAK,EAAE,IAAI,IAAI;AAE/B,OAAI,CAAC,WAAW,IAAI,IAAI,CAAE,YAAW,IAAI,qBAAK,IAAI,KAAK,CAAC;AACxD,cAAW,IAAI,IAAI,EAAE,IAAI,OAAO,KAAK;;;CAKzC,MAAM,2BAAW,IAAI,KAAqB;AAC1C,MAAK,MAAM,UAAU,QACnB,UAAS,IAAI,OAAO,MAAM,KAAK,IAAI,OAAO,KAAK,EAAE,QAAQ,EAAE;CAG7D,MAAM,QAAkB,EAAE;AAC1B,MAAK,MAAM,CAAC,MAAM,WAAW,SAC3B,KAAI,WAAW,EAAG,OAAM,KAAK,KAAK;AAGpC,OAAM,MAAM;CAEZ,MAAM,QAAkB,EAAE;AAE1B,QAAO,MAAM,SAAS,GAAG;AAEvB,QAAM,MAAM;EACZ,MAAM,OAAO,MAAM,OAAO;AAC1B,QAAM,KAAK,KAAK;AAEhB,OAAK,MAAM,aAAa,WAAW,IAAI,KAAK,IAAI,EAAE,EAAE;GAClD,MAAM,aAAa,SAAS,IAAI,UAAU,IAAI,KAAK;AACnD,YAAS,IAAI,WAAW,UAAU;AAClC,OAAI,cAAc,EAChB,OAAM,KAAK,UAAU;;;AAM3B,KAAI,MAAM,SAAS,QAAQ,QAAQ;EACjC,MAAM,YAAY,QAAQ,KAAK,MAAM,EAAE,KAAK,CAAC,QAAQ,MAAM,CAAC,MAAM,SAAS,EAAE,CAAC;AAC9E,UAAQ,KACN,6DAA6D,UAAU,KAAK,KAAK,CAAC,8CAEnF;AACD,QAAM,KAAK,GAAG,UAAU,MAAM,CAAC;;AAGjC,QAAO"}