alchemy 0.2.5 → 0.3.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.
Files changed (197) hide show
  1. package/README.md +3 -3
  2. package/lib/ai/approve.d.ts +99 -0
  3. package/lib/ai/approve.js +76 -0
  4. package/lib/ai/astro-file.d.ts +137 -0
  5. package/lib/ai/astro-file.js +156 -0
  6. package/lib/ai/client.d.ts +7 -8
  7. package/lib/ai/client.js +48 -19
  8. package/lib/ai/css-file.d.ts +131 -0
  9. package/lib/ai/css-file.js +141 -0
  10. package/lib/ai/data.d.ts +51 -6
  11. package/lib/ai/data.js +58 -12
  12. package/lib/ai/document.d.ts +108 -24
  13. package/lib/ai/document.js +166 -52
  14. package/lib/ai/html-file.d.ts +128 -0
  15. package/lib/ai/html-file.js +138 -0
  16. package/lib/ai/index.d.ts +9 -0
  17. package/lib/ai/index.js +9 -0
  18. package/lib/ai/json-file.d.ts +160 -0
  19. package/lib/ai/json-file.js +187 -0
  20. package/lib/ai/review.d.ts +122 -0
  21. package/lib/ai/review.js +101 -0
  22. package/lib/ai/typescript-file.d.ts +139 -0
  23. package/lib/ai/typescript-file.js +145 -0
  24. package/lib/ai/vue-file.d.ts +126 -0
  25. package/lib/ai/vue-file.js +136 -0
  26. package/lib/ai/yaml-file.d.ts +169 -0
  27. package/lib/ai/yaml-file.js +200 -0
  28. package/lib/alchemy.js +17 -2
  29. package/lib/apply.js +1 -1
  30. package/lib/cloudflare/dns.d.ts +88 -0
  31. package/lib/cloudflare/dns.js +192 -0
  32. package/lib/cloudflare/generate-asset-manifest.js +1 -1
  33. package/lib/cloudflare/index.d.ts +2 -1
  34. package/lib/cloudflare/index.js +2 -1
  35. package/lib/cloudflare/r2-rest-state-store.d.ts +125 -0
  36. package/lib/cloudflare/r2-rest-state-store.js +254 -0
  37. package/lib/cloudflare/response.d.ts +12 -0
  38. package/lib/cloudflare/response.js +0 -0
  39. package/lib/cloudflare/static-site-router.js +16 -7
  40. package/lib/cloudflare/static-site.js +3 -0
  41. package/lib/cloudflare/upload-asset-manifest.js +4 -2
  42. package/lib/cloudflare/zone.d.ts +18 -0
  43. package/lib/cloudflare/zone.js +17 -4
  44. package/lib/dns/godaddy.d.ts +7 -0
  45. package/lib/dns/godaddy.js +21 -0
  46. package/lib/dns/import-dns.d.ts +82 -0
  47. package/lib/dns/import-dns.js +114 -0
  48. package/lib/dns/index.d.ts +2 -0
  49. package/lib/dns/index.js +2 -0
  50. package/lib/dns/record.d.ts +88 -0
  51. package/lib/dns/record.js +13 -0
  52. package/lib/fs/copy-file.d.ts +54 -0
  53. package/lib/fs/copy-file.js +63 -0
  54. package/lib/fs/file-system-state-store.d.ts +17 -0
  55. package/lib/fs/file-system-state-store.js +87 -0
  56. package/lib/fs/file.d.ts +13 -0
  57. package/lib/fs/file.js +24 -5
  58. package/lib/fs/folder.d.ts +10 -0
  59. package/lib/fs/folder.js +2 -2
  60. package/lib/fs/index.d.ts +10 -4
  61. package/lib/fs/index.js +10 -4
  62. package/lib/fs/static-astro-file.d.ts +34 -0
  63. package/lib/fs/static-astro-file.js +39 -0
  64. package/lib/fs/static-css-file.d.ts +24 -0
  65. package/lib/fs/static-css-file.js +29 -0
  66. package/lib/fs/static-html-file.d.ts +31 -0
  67. package/lib/fs/static-html-file.js +36 -0
  68. package/lib/fs/{json-file.d.ts → static-json-file.d.ts} +3 -3
  69. package/lib/fs/static-json-file.js +15 -0
  70. package/lib/fs/{text-file.d.ts → static-text-file.d.ts} +2 -2
  71. package/lib/fs/static-text-file.js +8 -0
  72. package/lib/fs/{typescript-file.d.ts → static-typescript-file.d.ts} +3 -3
  73. package/lib/fs/{typescript-file.js → static-typescript-file.js} +3 -2
  74. package/lib/fs/static-vue-file.d.ts +28 -0
  75. package/lib/fs/static-vue-file.js +33 -0
  76. package/lib/fs/static-yaml-file.d.ts +22 -0
  77. package/lib/fs/{yaml-file.js → static-yaml-file.js} +3 -2
  78. package/lib/internal/getting-started.d.ts +21 -0
  79. package/lib/internal/getting-started.js +87 -0
  80. package/lib/internal/index.d.ts +3 -0
  81. package/lib/internal/index.js +3 -0
  82. package/lib/internal/providers.d.ts +27 -0
  83. package/lib/internal/providers.js +172 -0
  84. package/lib/internal/tutorial.d.ts +104 -0
  85. package/lib/internal/tutorial.js +251 -0
  86. package/lib/resource.js +8 -1
  87. package/lib/scope.d.ts +1 -1
  88. package/lib/scope.js +4 -2
  89. package/lib/state.d.ts +1 -16
  90. package/lib/state.js +0 -87
  91. package/lib/test/bun.d.ts +11 -0
  92. package/lib/test/bun.js +36 -8
  93. package/lib/web/astro.d.ts +147 -0
  94. package/lib/web/astro.js +414 -0
  95. package/lib/{shadcn/component.js → web/shadcn-component.js} +2 -9
  96. package/lib/web/shadcn.d.ts +97 -0
  97. package/lib/web/shadcn.js +99 -0
  98. package/lib/web/tailwind.d.ts +64 -0
  99. package/lib/web/tailwind.js +97 -0
  100. package/lib/{vite → web}/vite.d.ts +5 -0
  101. package/lib/{vite → web}/vite.js +44 -44
  102. package/lib/web/vitepress/config.d.ts +10 -0
  103. package/lib/web/vitepress/config.js +31 -0
  104. package/lib/web/vitepress/custom-theme.d.ts +173 -0
  105. package/lib/web/vitepress/custom-theme.js +336 -0
  106. package/lib/{vitepress → web/vitepress}/dependencies.d.ts +2 -2
  107. package/lib/{vitepress → web/vitepress}/dependencies.js +1 -1
  108. package/lib/web/vitepress/home-page.d.ts +246 -0
  109. package/lib/web/vitepress/home-page.js +147 -0
  110. package/lib/{vitepress → web/vitepress}/index.d.ts +2 -0
  111. package/lib/{vitepress → web/vitepress}/index.js +2 -0
  112. package/lib/{vitepress → web/vitepress}/vitepress.d.ts +15 -12
  113. package/lib/{vitepress → web/vitepress}/vitepress.js +26 -44
  114. package/package.json +6 -2
  115. package/src/ai/approve.ts +163 -0
  116. package/src/ai/astro-file.ts +269 -0
  117. package/src/ai/client.ts +60 -21
  118. package/src/ai/css-file.ts +247 -0
  119. package/src/ai/data.ts +81 -19
  120. package/src/ai/document.ts +240 -62
  121. package/src/ai/html-file.ts +244 -0
  122. package/src/ai/index.ts +9 -0
  123. package/src/ai/json-file.ts +325 -0
  124. package/src/ai/review.ts +213 -0
  125. package/src/ai/typescript-file.ts +259 -0
  126. package/src/ai/vue-file.ts +242 -0
  127. package/src/ai/yaml-file.ts +328 -0
  128. package/src/alchemy.ts +18 -7
  129. package/src/apply.ts +8 -9
  130. package/src/cloudflare/dns.ts +344 -0
  131. package/src/cloudflare/generate-asset-manifest.ts +4 -4
  132. package/src/cloudflare/index.ts +2 -1
  133. package/src/cloudflare/r2-rest-state-store.ts +353 -0
  134. package/src/cloudflare/response.ts +9 -0
  135. package/src/cloudflare/static-site-router.ts +18 -8
  136. package/src/cloudflare/static-site.ts +11 -7
  137. package/src/cloudflare/upload-asset-manifest.ts +6 -4
  138. package/src/cloudflare/worker.ts +44 -43
  139. package/src/cloudflare/zone.ts +39 -23
  140. package/src/dns/godaddy.ts +29 -0
  141. package/src/dns/import-dns.ts +213 -0
  142. package/src/dns/index.ts +2 -0
  143. package/src/dns/record.ts +121 -0
  144. package/src/fs/copy-file.ts +115 -0
  145. package/src/fs/file-system-state-store.ts +109 -0
  146. package/src/fs/file.ts +35 -9
  147. package/src/fs/folder.ts +14 -2
  148. package/src/fs/index.ts +10 -4
  149. package/src/fs/static-astro-file.ts +45 -0
  150. package/src/fs/static-css-file.ts +35 -0
  151. package/src/fs/static-html-file.ts +42 -0
  152. package/src/fs/static-json-file.ts +34 -0
  153. package/src/fs/{text-file.ts → static-text-file.ts} +9 -3
  154. package/src/fs/{typescript-file.ts → static-typescript-file.ts} +7 -6
  155. package/src/fs/static-vue-file.ts +39 -0
  156. package/src/fs/static-yaml-file.ts +33 -0
  157. package/src/internal/getting-started.ts +115 -0
  158. package/src/internal/index.ts +3 -0
  159. package/src/internal/providers.ts +241 -0
  160. package/src/internal/tutorial.ts +392 -0
  161. package/src/resource.ts +10 -1
  162. package/src/scope.ts +5 -6
  163. package/src/state.ts +1 -111
  164. package/src/test/bun.ts +60 -10
  165. package/src/web/astro.ts +644 -0
  166. package/src/{shadcn/component.ts → web/shadcn-component.ts} +5 -13
  167. package/src/web/shadcn.ts +219 -0
  168. package/src/web/tailwind.ts +167 -0
  169. package/src/{vite → web}/vite.ts +54 -56
  170. package/src/web/vitepress/config.ts +48 -0
  171. package/src/web/vitepress/custom-theme.ts +514 -0
  172. package/src/{vitepress → web/vitepress}/dependencies.ts +2 -2
  173. package/src/web/vitepress/home-page.ts +375 -0
  174. package/src/{vitepress → web/vitepress}/index.ts +2 -0
  175. package/src/{vitepress → web/vitepress}/vitepress.ts +62 -72
  176. package/lib/cloudflare/state.d.ts +0 -82
  177. package/lib/cloudflare/state.js +0 -108
  178. package/lib/fs/json-file.js +0 -7
  179. package/lib/fs/text-file.js +0 -7
  180. package/lib/fs/yaml-file.d.ts +0 -19
  181. package/lib/internal/docs.d.ts +0 -5
  182. package/lib/internal/docs.js +0 -287
  183. package/lib/shadcn/index.d.ts +0 -1
  184. package/lib/shadcn/index.js +0 -1
  185. package/lib/vite/index.d.ts +0 -1
  186. package/lib/vite/index.js +0 -1
  187. package/lib/vitepress/home-page.d.ts +0 -133
  188. package/lib/vitepress/home-page.js +0 -11
  189. package/src/cloudflare/state.ts +0 -138
  190. package/src/fs/json-file.ts +0 -23
  191. package/src/fs/yaml-file.ts +0 -26
  192. package/src/internal/docs.ts +0 -338
  193. package/src/shadcn/index.ts +0 -1
  194. package/src/vite/index.ts +0 -1
  195. package/src/vitepress/home-page.ts +0 -166
  196. /package/lib/{shadcn/component.d.ts → web/shadcn-component.d.ts} +0 -0
  197. /package/src/{vitepress → web/vitepress}/index.md +0 -0
@@ -1,15 +1,9 @@
1
- import { generateText } from "ai";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
1
+ import { generateText, type CoreMessage } from "ai";
4
2
  import type { Context } from "../context";
3
+ import { StaticTextFile } from "../fs/static-text-file";
5
4
  import { Resource } from "../resource";
6
5
  import type { Secret } from "../secret";
7
- import {
8
- type ModelConfig,
9
- createClient,
10
- getModelId,
11
- getModelOptions,
12
- } from "./client";
6
+ import { createModel, withRateLimitRetry, type ModelConfig } from "./client";
13
7
 
14
8
  /**
15
9
  * Properties for creating or updating a Document
@@ -21,9 +15,10 @@ export interface DocumentProps {
21
15
  title: string;
22
16
 
23
17
  /**
24
- * Path to the markdown document
18
+ * Optional path to the markdown document
19
+ * If provided, document will be written to this path
25
20
  */
26
- path: string;
21
+ path?: string;
27
22
 
28
23
  /**
29
24
  * Base URL for the OpenAI API
@@ -40,7 +35,27 @@ export interface DocumentProps {
40
35
  * ${alchemy.file("src/api.ts")}
41
36
  * `
42
37
  */
43
- prompt: string;
38
+ prompt?: string;
39
+
40
+ /**
41
+ * Message history for conversation-based generation
42
+ * If provided, this will be used instead of the prompt
43
+ * @example
44
+ * messages: [
45
+ * { role: "user", content: "Generate API documentation for this file" },
46
+ * { role: "assistant", content: "I'll create detailed API docs. What file should I document?" },
47
+ * { role: "user", content: "Please document src/api.ts" }
48
+ * ]
49
+ */
50
+ messages?: CoreMessage[];
51
+
52
+ /**
53
+ * System prompt for the model
54
+ * This is used to provide instructions to the model about how to format the response
55
+ * The default system prompt instructs the model to return a single markdown document inside ```md fences
56
+ * @default "You are a technical documentation writer. Create a single markdown document based on the user's requirements. Your response MUST include only a single markdown document inside ```md fences. Do not include any other text, explanations, or multiple code blocks."
57
+ */
58
+ system?: string;
44
59
 
45
60
  /**
46
61
  * OpenAI API key to use for generating content
@@ -52,6 +67,21 @@ export interface DocumentProps {
52
67
  * Model configuration
53
68
  */
54
69
  model?: ModelConfig;
70
+
71
+ /**
72
+ * Temperature for controlling randomness in generation.
73
+ * Higher values (e.g., 0.8) make output more random,
74
+ * lower values (e.g., 0.2) make it more deterministic.
75
+ * @default 0.7
76
+ */
77
+ temperature?: number;
78
+
79
+ /**
80
+ * Maximum number of tokens to generate.
81
+ * Higher values allow for longer documents but may increase cost and generation time.
82
+ * @default 10000
83
+ */
84
+ maxTokens?: number;
55
85
  }
56
86
 
57
87
  /**
@@ -63,6 +93,11 @@ export interface Document extends DocumentProps, Resource<"docs::Document"> {
63
93
  */
64
94
  content: string;
65
95
 
96
+ /**
97
+ * Updated message history with the document response appended
98
+ */
99
+ messages: CoreMessage[];
100
+
66
101
  /**
67
102
  * Time at which the document was created
68
103
  */
@@ -72,48 +107,101 @@ export interface Document extends DocumentProps, Resource<"docs::Document"> {
72
107
  * Time at which the document was last updated
73
108
  */
74
109
  updatedAt: number;
110
+
111
+ /**
112
+ * File resource if path was provided
113
+ */
114
+ file?: StaticTextFile;
75
115
  }
76
116
 
117
+ /**
118
+ * Default system prompt for markdown document generation
119
+ */
120
+ const DEFAULT_MD_SYSTEM_PROMPT =
121
+ "You are a technical documentation writer. Create a single markdown document based on the user's requirements. Your response MUST include only a single markdown document inside ```md fences. Do not include any other text, explanations, or multiple code blocks.";
122
+
77
123
  /**
78
124
  * Resource for managing AI-generated markdown documents using the Vercel AI SDK.
79
125
  * Supports powerful context handling through the alchemy template literal tag.
80
126
  *
81
127
  * @example
82
- * // Create a document using alchemy template literals for context
128
+ * // Create an in-memory document (no file created)
129
+ * const apiDocs = await Document("api-docs", {
130
+ * title: "API Documentation",
131
+ * prompt: await alchemy`
132
+ * Generate API documentation based on these source files:
133
+ * ${alchemy.file("src/api.ts")}
134
+ * ${alchemy.file("src/types.ts")}
135
+ * `,
136
+ * model: {
137
+ * id: "gpt-4o",
138
+ * provider: "openai"
139
+ * }
140
+ * });
141
+ *
142
+ * @example
143
+ * // Create a document and write it to disk
83
144
  * const apiDocs = await Document("api-docs", {
145
+ * title: "API Documentation",
84
146
  * path: "./docs/api.md",
85
147
  * prompt: await alchemy`
86
148
  * Generate API documentation based on these source files:
87
149
  * ${alchemy.file("src/api.ts")}
88
150
  * ${alchemy.file("src/types.ts")}
89
- * `
90
- * // The above will automatically append the file contents as code blocks:
91
- * //
92
- * // Generate API documentation based on these source files:
93
- * // [api.ts](src/api.ts)
94
- * // [types.ts](src/types.ts)
95
- * //
96
- * // // src/api.ts
97
- * // ```ts
98
- * // ... contents of api.ts ...
99
- * // ```
100
- * //
101
- * // // src/types.ts
102
- * // ```ts
103
- * // ... contents of types.ts ...
104
- * // ```
151
+ * `,
152
+ * model: {
153
+ * id: "gpt-4o",
154
+ * provider: "openai"
155
+ * }
156
+ * });
157
+ *
158
+ * @example
159
+ * // Use message history for iterative document generation
160
+ * const apiDocs = await Document("api-docs", {
161
+ * title: "API Documentation",
162
+ * path: "./docs/api.md",
163
+ * messages: [
164
+ * { role: "user", content: "Create API documentation for these files" },
165
+ * { role: "assistant", content: "I'll help you create API documentation. Please provide the files." },
166
+ * { role: "user", content: "Here are the files: [file contents]" }
167
+ * ],
168
+ * system: "You are a technical documentation writer. Generate clear and concise API documentation.",
169
+ * model: {
170
+ * id: "gpt-4o",
171
+ * provider: "openai"
172
+ * }
105
173
  * });
106
174
  *
107
175
  * @example
108
- * // Use alchemy template literals with file collections
176
+ * // Use alchemy template literals with file collections and temperature control
109
177
  * const modelDocs = await Document("models", {
178
+ * title: "Data Models",
110
179
  * path: "./docs/models.md",
111
180
  * prompt: await alchemy`
112
181
  * Write documentation for these data models:
113
182
  * ${alchemy.files("src/models/user.ts", "src/models/post.ts")}
114
- * `
115
- * // This creates a prompt with all files appended as code blocks,
116
- * // automatically handling syntax highlighting based on file extensions
183
+ * `,
184
+ * temperature: 0.2 // Lower temperature for more deterministic output
185
+ * });
186
+ *
187
+ * @example
188
+ * // Advanced model configuration with custom provider options and custom system prompt
189
+ * const techDocs = await Document("tech-specs", {
190
+ * title: "Technical Specifications",
191
+ * path: "./docs/tech-specs.md",
192
+ * prompt: await alchemy`
193
+ * Create detailed technical specifications based on these requirements:
194
+ * ${alchemy.file("requirements/system.md")}
195
+ * `,
196
+ * system: "You are an expert technical writer specializing in system specifications. Create a single markdown document inside ```md fences with no additional text.",
197
+ * model: {
198
+ * id: "o3-mini",
199
+ * provider: "openai",
200
+ * options: {
201
+ * reasoningEffort: "high"
202
+ * }
203
+ * },
204
+ * temperature: 0.1
117
205
  * });
118
206
  */
119
207
  export const Document = Resource(
@@ -121,45 +209,135 @@ export const Document = Resource(
121
209
  async function (
122
210
  this: Context<Document>,
123
211
  id: string,
124
- props: DocumentProps,
212
+ props: DocumentProps
125
213
  ): Promise<Document> {
126
- // Ensure directory exists
127
- await fs.mkdir(path.dirname(props.path), { recursive: true });
214
+ // Validate that either prompt or messages are provided
215
+ if (!props.prompt && !props.messages) {
216
+ throw new Error("Either prompt or messages must be provided");
217
+ }
128
218
 
219
+ // Handle deletion phase
129
220
  if (this.phase === "delete") {
130
- try {
131
- await fs.unlink(props.path);
132
- } catch (error: any) {
133
- // Ignore if file doesn't exist
134
- if (error.code !== "ENOENT") {
135
- throw error;
136
- }
137
- }
138
221
  return this.destroy();
139
222
  }
140
223
 
141
- // Initialize OpenAI compatible provider using shared client
142
- const provider = createClient(props);
224
+ // Use provided system prompt or default
225
+ const system = props.system || DEFAULT_MD_SYSTEM_PROMPT;
143
226
 
144
- // Generate content
145
- const { text } = await generateText({
146
- model: provider(getModelId(props)),
147
- prompt: props.prompt,
148
- ...getModelOptions(props),
227
+ // Generate initial content with rate limit retry
228
+ const { text } = await withRateLimitRetry(async () => {
229
+ return generateText({
230
+ model: createModel(props),
231
+ ...(props.messages
232
+ ? { messages: props.messages }
233
+ : { prompt: props.prompt! }),
234
+ system,
235
+ maxTokens: props.maxTokens || 8192,
236
+ providerOptions: props.model?.options,
237
+ ...(props.temperature === undefined
238
+ ? {}
239
+ : // some models error if you provide it (rather than ignoring it)
240
+ { temperature: props.temperature }),
241
+ });
149
242
  });
150
243
 
151
- // Write content to file
152
- await fs.writeFile(props.path, text);
244
+ // Extract and validate markdown content
245
+ let { content, error } = extractMarkdownContent(text);
153
246
 
154
- // Get file stats for timestamps
155
- const stats = await fs.stat(props.path);
247
+ // Re-prompt if there are validation errors
248
+ if (error) {
249
+ const errorSystem = `${system}\n\nERROR: ${error}\n\nPlease try again and ensure your response contains exactly one markdown document inside \`\`\`md fences.`;
156
250
 
157
- // Return the resource
158
- return this({
251
+ const { text: retryText } = await withRateLimitRetry(async () => {
252
+ return generateText({
253
+ model: createModel(props),
254
+ ...(props.messages
255
+ ? { messages: props.messages }
256
+ : { prompt: props.prompt! }),
257
+ system: errorSystem,
258
+ providerOptions: props.model?.options,
259
+ ...(props.temperature === undefined
260
+ ? {}
261
+ : { temperature: props.temperature }),
262
+ });
263
+ });
264
+
265
+ const retryResult = extractMarkdownContent(retryText);
266
+
267
+ if (retryResult.error) {
268
+ throw new Error(
269
+ `Failed to generate valid markdown content: ${retryResult.error}`
270
+ );
271
+ }
272
+
273
+ content = retryResult.content;
274
+ }
275
+
276
+ // Create result object
277
+ const result: Partial<Document> = {
159
278
  ...props,
160
- content: text,
161
- createdAt: stats.birthtimeMs,
162
- updatedAt: stats.mtimeMs,
163
- });
164
- },
279
+ content,
280
+ messages: [
281
+ ...(props.messages || [{ role: "user", content: props.prompt! }]),
282
+ { role: "assistant", content },
283
+ ],
284
+ createdAt: this.output?.createdAt || Date.now(),
285
+ updatedAt: Date.now(),
286
+ };
287
+
288
+ // Write file if path is provided
289
+ if (props.path) {
290
+ const filePath = props.path;
291
+ const fileId = `${id}-file`;
292
+
293
+ result.file = await StaticTextFile(fileId, filePath, content);
294
+ }
295
+
296
+ // Return the resource
297
+ return this(result as Document);
298
+ }
165
299
  );
300
+
301
+ /**
302
+ * Extracts markdown content from between ```md fences
303
+ * Validates that exactly one markdown code block exists
304
+ *
305
+ * @param text The text to extract markdown content from
306
+ * @returns The extracted markdown content or error message
307
+ */
308
+ function extractMarkdownContent(text: string): {
309
+ content: string;
310
+ error?: string;
311
+ } {
312
+ const lines = text.split("\n");
313
+ const startIdx = lines.findIndex((line) => line.trim() === "```md");
314
+
315
+ if (startIdx === -1) {
316
+ return {
317
+ content: "",
318
+ error:
319
+ "No markdown code block found in the response. Please include your markdown content within ```md fences.",
320
+ };
321
+ }
322
+
323
+ const rest = lines.slice(startIdx + 1);
324
+ const endRelativeIdx = rest
325
+ .map((line) => line.trim() === "```")
326
+ .lastIndexOf(true);
327
+
328
+ if (endRelativeIdx === -1) {
329
+ return {
330
+ content: "",
331
+ error: "Markdown block was not closed properly.",
332
+ };
333
+ }
334
+
335
+ const endIdx = startIdx + 1 + endRelativeIdx;
336
+
337
+ const content = lines
338
+ .slice(startIdx + 1, endIdx)
339
+ .join("\n")
340
+ .trim();
341
+
342
+ return { content };
343
+ }
@@ -0,0 +1,244 @@
1
+ import { generateText } from "ai";
2
+ import type { Context } from "../context";
3
+ import { StaticHTMLFile } from "../fs/static-html-file";
4
+ import { Resource } from "../resource";
5
+ import type { Secret } from "../secret";
6
+ import { type ModelConfig, createModel } from "./client";
7
+
8
+ /**
9
+ * Properties for creating or updating an HTMLFile
10
+ */
11
+ export interface HTMLFileProps {
12
+ /**
13
+ * Path to the HTML file
14
+ */
15
+ path: string;
16
+
17
+ /**
18
+ * Base URL for the OpenAI API
19
+ * @default 'https://api.openai.com/v1'
20
+ */
21
+ baseURL?: string;
22
+
23
+ /**
24
+ * Prompt for generating content
25
+ * Use alchemy template literals to include file context:
26
+ * @example
27
+ * prompt: await alchemy`
28
+ * Generate an HTML page using:
29
+ * ${alchemy.file("src/templates/base.html")}
30
+ * `
31
+ */
32
+ prompt: string;
33
+
34
+ /**
35
+ * System prompt for the model
36
+ * This is used to provide instructions to the model about how to format the response
37
+ * The default system prompt instructs the model to return HTML code inside ```html fences
38
+ * @default "You are an HTML code generator. Create HTML code based on the user's requirements. Your response MUST include only HTML code inside ```html fences. Do not include any other text, explanations, or multiple code blocks."
39
+ */
40
+ system?: string;
41
+
42
+ /**
43
+ * OpenAI API key to use for generating content
44
+ * If not provided, will use OPENAI_API_KEY environment variable
45
+ */
46
+ apiKey?: Secret;
47
+
48
+ /**
49
+ * Model configuration
50
+ */
51
+ model?: ModelConfig;
52
+
53
+ /**
54
+ * Temperature for controlling randomness in generation.
55
+ * Higher values (e.g., 0.8) make output more random,
56
+ * lower values (e.g., 0.2) make it more deterministic.
57
+ * @default 0.7
58
+ */
59
+ temperature?: number;
60
+ }
61
+
62
+ /**
63
+ * An HTML file that can be created, updated, and deleted
64
+ */
65
+ export interface HTMLFile extends HTMLFileProps, Resource<"ai::HTMLFile"> {
66
+ /**
67
+ * Content of the HTML file
68
+ */
69
+ content: string;
70
+
71
+ /**
72
+ * Time at which the file was created
73
+ */
74
+ createdAt: number;
75
+
76
+ /**
77
+ * Time at which the file was last updated
78
+ */
79
+ updatedAt: number;
80
+ }
81
+
82
+ /**
83
+ * Default system prompt for HTML file generation
84
+ */
85
+ const DEFAULT_HTML_SYSTEM_PROMPT =
86
+ "You are an HTML code generator. Create HTML code based on the user's requirements. Your response MUST include only HTML code inside ```html fences. Do not include any other text, explanations, or multiple code blocks.";
87
+
88
+ /**
89
+ * Resource for generating HTML files using AI models.
90
+ * Extracts HTML code from between ```html fences and validates the response.
91
+ *
92
+ * @example
93
+ * // Create a simple landing page
94
+ * const landingPage = await HTMLFile("landing-page", {
95
+ * path: "./public/index.html",
96
+ * prompt: await alchemy`
97
+ * Generate a modern landing page for a SaaS product with:
98
+ * - Hero section with headline and call-to-action
99
+ * - Features section with 3 key features
100
+ * - Pricing section with 3 tiers
101
+ * - Testimonials section with 2 customer quotes
102
+ * - Contact form and footer
103
+ * `,
104
+ * model: {
105
+ * id: "gpt-4o",
106
+ * provider: "openai"
107
+ * }
108
+ * });
109
+ *
110
+ * @example
111
+ * // Generate an HTML email template
112
+ * const emailTemplate = await HTMLFile("welcome-email", {
113
+ * path: "./emails/welcome.html",
114
+ * prompt: await alchemy`
115
+ * Create an HTML email template for welcoming new users to our platform.
116
+ * The email should include:
117
+ * - Company logo and branding
118
+ * - Personalized welcome message (use {{name}} placeholder)
119
+ * - Three steps to get started
120
+ * - Support contact information
121
+ * - Unsubscribe footer
122
+ *
123
+ * Make sure it's responsive and works in all major email clients.
124
+ * `,
125
+ * temperature: 0.2
126
+ * });
127
+ *
128
+ * @example
129
+ * // Generate an HTML component with custom system prompt
130
+ * const navComponent = await HTMLFile("navigation", {
131
+ * path: "./components/nav.html",
132
+ * prompt: await alchemy`
133
+ * Create a responsive navigation component with:
134
+ * - Logo in the left corner
135
+ * - Navigation links: Home, Products, Services, About, Contact
136
+ * - Mobile hamburger menu that expands/collapses
137
+ * - Login/signup buttons on the right side
138
+ * - Dark/light mode toggle
139
+ * `,
140
+ * system: "You are an expert HTML/CSS developer specializing in responsive components. Create a single HTML file inside ```html fences with no additional text. Use modern HTML5 semantic elements and inline CSS if needed.",
141
+ * model: {
142
+ * id: "claude-3-opus-20240229",
143
+ * provider: "anthropic"
144
+ * }
145
+ * });
146
+ */
147
+ export const HTMLFile = Resource(
148
+ "ai::HTMLFile",
149
+ async function (
150
+ this: Context<HTMLFile>,
151
+ id: string,
152
+ props: HTMLFileProps
153
+ ): Promise<HTMLFile> {
154
+ // Handle deletion phase
155
+ if (this.phase === "delete") {
156
+ return this.destroy();
157
+ }
158
+
159
+ // Use provided system prompt or default
160
+ const system = props.system || DEFAULT_HTML_SYSTEM_PROMPT;
161
+
162
+ // Generate initial content
163
+ const { text } = await generateText({
164
+ model: createModel(props),
165
+ prompt: props.prompt,
166
+ system,
167
+ providerOptions: props.model?.options,
168
+ ...(props.temperature === undefined
169
+ ? {}
170
+ : { temperature: props.temperature }),
171
+ });
172
+
173
+ // Extract and validate HTML code
174
+ let { code, error } = await extractHTMLCode(text);
175
+
176
+ // Re-prompt if there are validation errors
177
+ if (error) {
178
+ const errorSystem = `${system}\n\nERROR: ${error}\n\nPlease try again and ensure your response contains exactly one HTML code block inside \`\`\`html fences.`;
179
+
180
+ const { text: retryText } = await generateText({
181
+ model: createModel(props),
182
+ prompt: props.prompt,
183
+ system: errorSystem,
184
+ providerOptions: props.model?.options,
185
+ ...(props.temperature === undefined
186
+ ? {}
187
+ : { temperature: props.temperature }),
188
+ });
189
+
190
+ const retryResult = await extractHTMLCode(retryText);
191
+
192
+ if (retryResult.error) {
193
+ throw new Error(
194
+ `Failed to generate valid HTML code: ${retryResult.error}`
195
+ );
196
+ }
197
+
198
+ code = retryResult.code;
199
+ }
200
+
201
+ // Use StaticHTMLFile to create/update the file
202
+ const file = await StaticHTMLFile("file", props.path, code);
203
+
204
+ // Return the resource
205
+ return this({
206
+ ...props,
207
+ content: file.content,
208
+ createdAt: Date.now(),
209
+ updatedAt: Date.now(),
210
+ });
211
+ }
212
+ );
213
+
214
+ /**
215
+ * Extracts HTML code from between ```html fences
216
+ * Validates that exactly one HTML code block exists
217
+ *
218
+ * @param text The text to extract HTML code from
219
+ * @returns The extracted HTML code or error message
220
+ */
221
+ async function extractHTMLCode(
222
+ text: string
223
+ ): Promise<{ code: string; error?: string }> {
224
+ const htmlCodeRegex = /```html\s*([\s\S]*?)```/g;
225
+ const matches = Array.from(text.matchAll(htmlCodeRegex));
226
+
227
+ if (matches.length === 0) {
228
+ return {
229
+ code: "",
230
+ error:
231
+ "No HTML code block found in the response. Please include your code within ```html fences.",
232
+ };
233
+ }
234
+
235
+ if (matches.length > 1) {
236
+ return {
237
+ code: "",
238
+ error:
239
+ "Multiple HTML code blocks found in the response. Please provide exactly one code block within ```html fences.",
240
+ };
241
+ }
242
+
243
+ return { code: matches[0][1].trim() };
244
+ }
package/src/ai/index.ts CHANGED
@@ -1,3 +1,12 @@
1
+ export * from "./approve";
1
2
  export * from "./ark";
3
+ export * from "./astro-file";
4
+ export * from "./css-file";
2
5
  export * from "./data";
3
6
  export * from "./document";
7
+ export * from "./html-file";
8
+ export * from "./json-file";
9
+ export * from "./review";
10
+ export * from "./typescript-file";
11
+ export * from "./vue-file";
12
+ export * from "./yaml-file";