alchemy 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/README.md +3 -3
  2. package/lib/ai/astro-file.d.ts +137 -0
  3. package/lib/ai/astro-file.js +173 -0
  4. package/lib/ai/client.d.ts +2 -10
  5. package/lib/ai/client.js +8 -24
  6. package/lib/ai/css-file.d.ts +131 -0
  7. package/lib/ai/css-file.js +158 -0
  8. package/lib/ai/data.d.ts +45 -5
  9. package/lib/ai/data.js +45 -10
  10. package/lib/ai/document.d.ts +44 -20
  11. package/lib/ai/document.js +95 -28
  12. package/lib/ai/html-file.d.ts +128 -0
  13. package/lib/ai/html-file.js +155 -0
  14. package/lib/ai/index.d.ts +7 -0
  15. package/lib/ai/index.js +7 -0
  16. package/lib/ai/json-file.d.ts +160 -0
  17. package/lib/ai/json-file.js +208 -0
  18. package/lib/ai/typescript-file.d.ts +140 -0
  19. package/lib/ai/typescript-file.js +176 -0
  20. package/lib/ai/vue-file.d.ts +126 -0
  21. package/lib/ai/vue-file.js +153 -0
  22. package/lib/ai/yaml-file.d.ts +169 -0
  23. package/lib/ai/yaml-file.js +236 -0
  24. package/lib/cloudflare/dns.d.ts +88 -0
  25. package/lib/cloudflare/dns.js +192 -0
  26. package/lib/cloudflare/index.d.ts +2 -1
  27. package/lib/cloudflare/index.js +2 -1
  28. package/lib/cloudflare/r2-rest-state-store.d.ts +125 -0
  29. package/lib/cloudflare/r2-rest-state-store.js +254 -0
  30. package/lib/cloudflare/response.d.ts +12 -0
  31. package/lib/cloudflare/response.js +0 -0
  32. package/lib/cloudflare/zone.d.ts +18 -0
  33. package/lib/cloudflare/zone.js +17 -4
  34. package/lib/dns/godaddy.d.ts +7 -0
  35. package/lib/dns/godaddy.js +21 -0
  36. package/lib/dns/import-dns.d.ts +82 -0
  37. package/lib/dns/import-dns.js +114 -0
  38. package/lib/dns/index.d.ts +2 -0
  39. package/lib/dns/index.js +2 -0
  40. package/lib/dns/record.d.ts +88 -0
  41. package/lib/dns/record.js +13 -0
  42. package/lib/fs/file-system-state-store.d.ts +17 -0
  43. package/lib/fs/file-system-state-store.js +87 -0
  44. package/lib/fs/folder.d.ts +10 -0
  45. package/lib/fs/folder.js +2 -2
  46. package/lib/fs/index.d.ts +4 -4
  47. package/lib/fs/index.js +4 -4
  48. package/lib/fs/{json-file.d.ts → static-json-file.d.ts} +2 -2
  49. package/lib/fs/static-json-file.js +14 -0
  50. package/lib/fs/{text-file.d.ts → static-text-file.d.ts} +2 -2
  51. package/lib/fs/{text-file.js → static-text-file.js} +1 -1
  52. package/lib/fs/{typescript-file.d.ts → static-typescript-file.d.ts} +2 -2
  53. package/lib/fs/{typescript-file.js → static-typescript-file.js} +1 -1
  54. package/lib/fs/{yaml-file.d.ts → static-yaml-file.d.ts} +2 -2
  55. package/lib/fs/{yaml-file.js → static-yaml-file.js} +1 -1
  56. package/lib/internal/getting-started.d.ts +10 -0
  57. package/lib/internal/getting-started.js +94 -0
  58. package/lib/internal/project.d.ts +13 -0
  59. package/lib/internal/project.js +101 -0
  60. package/lib/internal/providers.d.ts +23 -0
  61. package/lib/internal/providers.js +148 -0
  62. package/lib/resource.js +8 -1
  63. package/lib/scope.d.ts +1 -1
  64. package/lib/scope.js +4 -2
  65. package/lib/state.d.ts +1 -16
  66. package/lib/state.js +0 -87
  67. package/lib/test/bun.d.ts +11 -0
  68. package/lib/test/bun.js +36 -8
  69. package/lib/web/astro.d.ts +147 -0
  70. package/lib/web/astro.js +414 -0
  71. package/lib/{shadcn/component.js → web/shadcn-component.js} +2 -9
  72. package/lib/web/shadcn.d.ts +97 -0
  73. package/lib/web/shadcn.js +99 -0
  74. package/lib/web/tailwind.d.ts +64 -0
  75. package/lib/web/tailwind.js +97 -0
  76. package/lib/{vite → web}/vite.d.ts +5 -0
  77. package/lib/{vite → web}/vite.js +44 -44
  78. package/lib/web/vitepress/custom-theme.d.ts +173 -0
  79. package/lib/web/vitepress/custom-theme.js +336 -0
  80. package/lib/{vitepress → web/vitepress}/dependencies.d.ts +2 -2
  81. package/lib/{vitepress → web/vitepress}/dependencies.js +1 -1
  82. package/lib/web/vitepress/home-page.d.ts +244 -0
  83. package/lib/web/vitepress/home-page.js +134 -0
  84. package/lib/{vitepress → web/vitepress}/index.d.ts +1 -0
  85. package/lib/{vitepress → web/vitepress}/index.js +1 -0
  86. package/lib/{vitepress → web/vitepress}/vitepress.d.ts +10 -8
  87. package/lib/{vitepress → web/vitepress}/vitepress.js +11 -12
  88. package/package.json +6 -2
  89. package/src/ai/astro-file.ts +288 -0
  90. package/src/ai/client.ts +8 -28
  91. package/src/ai/css-file.ts +266 -0
  92. package/src/ai/data.ts +53 -16
  93. package/src/ai/document.ts +130 -33
  94. package/src/ai/html-file.ts +263 -0
  95. package/src/ai/index.ts +7 -0
  96. package/src/ai/json-file.ts +349 -0
  97. package/src/ai/typescript-file.ts +293 -0
  98. package/src/ai/vue-file.ts +261 -0
  99. package/src/ai/yaml-file.ts +368 -0
  100. package/src/apply.ts +0 -1
  101. package/src/cloudflare/dns.ts +344 -0
  102. package/src/cloudflare/index.ts +2 -1
  103. package/src/cloudflare/r2-rest-state-store.ts +353 -0
  104. package/src/cloudflare/response.ts +9 -0
  105. package/src/cloudflare/zone.ts +39 -23
  106. package/src/dns/godaddy.ts +29 -0
  107. package/src/dns/import-dns.ts +213 -0
  108. package/src/dns/index.ts +2 -0
  109. package/src/dns/record.ts +121 -0
  110. package/src/fs/file-system-state-store.ts +109 -0
  111. package/src/fs/folder.ts +14 -2
  112. package/src/fs/index.ts +4 -4
  113. package/src/fs/{json-file.ts → static-json-file.ts} +13 -3
  114. package/src/fs/{text-file.ts → static-text-file.ts} +5 -2
  115. package/src/fs/{typescript-file.ts → static-typescript-file.ts} +3 -3
  116. package/src/fs/{yaml-file.ts → static-yaml-file.ts} +5 -2
  117. package/src/internal/getting-started.ts +107 -0
  118. package/src/internal/project.ts +119 -0
  119. package/src/internal/providers.ts +209 -0
  120. package/src/resource.ts +10 -1
  121. package/src/scope.ts +5 -6
  122. package/src/state.ts +1 -111
  123. package/src/test/bun.ts +60 -10
  124. package/src/web/astro.ts +644 -0
  125. package/src/{shadcn/component.ts → web/shadcn-component.ts} +5 -13
  126. package/src/web/shadcn.ts +219 -0
  127. package/src/web/tailwind.ts +167 -0
  128. package/src/{vite → web}/vite.ts +54 -56
  129. package/src/web/vitepress/custom-theme.ts +514 -0
  130. package/src/{vitepress → web/vitepress}/dependencies.ts +2 -2
  131. package/src/web/vitepress/home-page.ts +363 -0
  132. package/src/{vitepress → web/vitepress}/index.ts +1 -0
  133. package/src/{vitepress → web/vitepress}/vitepress.ts +34 -27
  134. package/lib/cloudflare/state.d.ts +0 -82
  135. package/lib/cloudflare/state.js +0 -108
  136. package/lib/fs/json-file.js +0 -7
  137. package/lib/internal/docs.d.ts +0 -5
  138. package/lib/internal/docs.js +0 -287
  139. package/lib/shadcn/index.d.ts +0 -1
  140. package/lib/shadcn/index.js +0 -1
  141. package/lib/vite/index.d.ts +0 -1
  142. package/lib/vite/index.js +0 -1
  143. package/lib/vitepress/home-page.d.ts +0 -133
  144. package/lib/vitepress/home-page.js +0 -11
  145. package/src/cloudflare/state.ts +0 -138
  146. package/src/internal/docs.ts +0 -338
  147. package/src/shadcn/index.ts +0 -1
  148. package/src/vite/index.ts +0 -1
  149. package/src/vitepress/home-page.ts +0 -166
  150. /package/lib/{shadcn/component.d.ts → web/shadcn-component.d.ts} +0 -0
  151. /package/src/{vitepress → web/vitepress}/index.md +0 -0
@@ -0,0 +1,266 @@
1
+ import { generateText } from "ai";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import type { Context } from "../context";
5
+ import { Resource } from "../resource";
6
+ import type { Secret } from "../secret";
7
+ import { ignore } from "../util/ignore";
8
+ import { type ModelConfig, createModel } from "./client";
9
+
10
+ /**
11
+ * Properties for creating or updating a CSSFile
12
+ */
13
+ export interface CSSFileProps {
14
+ /**
15
+ * Path to the CSS file
16
+ */
17
+ path: string;
18
+
19
+ /**
20
+ * Base URL for the OpenAI API
21
+ * @default 'https://api.openai.com/v1'
22
+ */
23
+ baseURL?: string;
24
+
25
+ /**
26
+ * Prompt for generating content
27
+ * Use alchemy template literals to include file context:
28
+ * @example
29
+ * prompt: await alchemy`
30
+ * Generate CSS styles for:
31
+ * ${alchemy.file("src/components/Button.jsx")}
32
+ * `
33
+ */
34
+ prompt: string;
35
+
36
+ /**
37
+ * System prompt for the model
38
+ * This is used to provide instructions to the model about how to format the response
39
+ * The default system prompt instructs the model to return CSS code inside ```css fences
40
+ * @default "You are a CSS code generator. Create CSS code based on the user's requirements. Your response MUST include only CSS code inside ```css fences. Do not include any other text, explanations, or multiple code blocks."
41
+ */
42
+ system?: string;
43
+
44
+ /**
45
+ * OpenAI API key to use for generating content
46
+ * If not provided, will use OPENAI_API_KEY environment variable
47
+ */
48
+ apiKey?: Secret;
49
+
50
+ /**
51
+ * Model configuration
52
+ */
53
+ model?: ModelConfig;
54
+
55
+ /**
56
+ * Temperature for controlling randomness in generation.
57
+ * Higher values (e.g., 0.8) make output more random,
58
+ * lower values (e.g., 0.2) make it more deterministic.
59
+ * @default 0.7
60
+ */
61
+ temperature?: number;
62
+ }
63
+
64
+ /**
65
+ * A CSS file that can be created, updated, and deleted
66
+ */
67
+ export interface CSSFile extends CSSFileProps, Resource<"ai::CSSFile"> {
68
+ /**
69
+ * Content of the CSS file
70
+ */
71
+ content: string;
72
+
73
+ /**
74
+ * Time at which the file was created
75
+ */
76
+ createdAt: number;
77
+
78
+ /**
79
+ * Time at which the file was last updated
80
+ */
81
+ updatedAt: number;
82
+ }
83
+
84
+ /**
85
+ * Default system prompt for CSS file generation
86
+ */
87
+ const DEFAULT_CSS_SYSTEM_PROMPT =
88
+ "You are a CSS code generator. Create CSS code based on the user's requirements. Your response MUST include only CSS code inside ```css fences. Do not include any other text, explanations, or multiple code blocks.";
89
+
90
+ /**
91
+ * Resource for generating CSS files using AI models.
92
+ * Extracts CSS code from between ```css fences and validates the response.
93
+ *
94
+ * @example
95
+ * // Create styles for a website
96
+ * const mainStyles = await CSSFile("main-styles", {
97
+ * path: "./public/css/main.css",
98
+ * prompt: await alchemy`
99
+ * Generate modern CSS styles for a company website with:
100
+ * - Clean, minimalist design
101
+ * - Primary color: #0062ff
102
+ * - Secondary color: #6c757d
103
+ * - Light gray background
104
+ * - Responsive layout for mobile, tablet, and desktop
105
+ * - Custom styles for buttons, cards, and navigation
106
+ * `,
107
+ * model: {
108
+ * id: "gpt-4o",
109
+ * provider: "openai"
110
+ * }
111
+ * });
112
+ *
113
+ * @example
114
+ * // Generate CSS based on existing HTML
115
+ * const componentStyles = await CSSFile("component-styles", {
116
+ * path: "./src/styles/component.css",
117
+ * prompt: await alchemy`
118
+ * Create CSS styles for this HTML component:
119
+ * ${alchemy.file("src/components/Card.html")}
120
+ *
121
+ * The styles should be:
122
+ * - Modern and clean
123
+ * - Include hover effects and transitions
124
+ * - Support both light and dark themes
125
+ * - Use CSS variables for colors and spacing
126
+ * `,
127
+ * temperature: 0.2
128
+ * });
129
+ *
130
+ * @example
131
+ * // Generate CSS animation with custom system prompt
132
+ * const animationStyles = await CSSFile("animations", {
133
+ * path: "./src/styles/animations.css",
134
+ * prompt: await alchemy`
135
+ * Create CSS animations for:
136
+ * - Fade in/out
137
+ * - Slide in from different directions
138
+ * - Pulse effect
139
+ * - Bounce effect
140
+ * - Scale in/out
141
+ * - Rotate
142
+ *
143
+ * Each animation should be reusable via class names.
144
+ * `,
145
+ * system: "You are an expert CSS animator. Create a single CSS file inside ```css fences with no additional text. Use modern CSS animation techniques and include vendor prefixes where needed for browser compatibility.",
146
+ * model: {
147
+ * id: "claude-3-opus-20240229",
148
+ * provider: "anthropic"
149
+ * }
150
+ * });
151
+ */
152
+ export const CSSFile = Resource(
153
+ "ai::CSSFile",
154
+ async function (
155
+ this: Context<CSSFile>,
156
+ id: string,
157
+ props: CSSFileProps,
158
+ ): Promise<CSSFile> {
159
+ // Ensure directory exists
160
+ await fs.mkdir(path.dirname(props.path), { recursive: true });
161
+
162
+ if (this.phase === "delete") {
163
+ try {
164
+ await fs.unlink(props.path);
165
+ } catch (error: any) {
166
+ // Ignore if file doesn't exist
167
+ if (error.code !== "ENOENT") {
168
+ throw error;
169
+ }
170
+ }
171
+ return this.destroy();
172
+ }
173
+
174
+ // Use provided system prompt or default
175
+ const system = props.system || DEFAULT_CSS_SYSTEM_PROMPT;
176
+
177
+ // Generate initial content
178
+ const { text } = await generateText({
179
+ model: createModel(props),
180
+ prompt: props.prompt,
181
+ system,
182
+ providerOptions: props.model?.options,
183
+ ...(props.temperature === undefined
184
+ ? {}
185
+ : { temperature: props.temperature }),
186
+ });
187
+
188
+ // Extract and validate CSS code
189
+ let { code, error } = await extractCSSCode(text);
190
+
191
+ // Re-prompt if there are validation errors
192
+ if (error) {
193
+ const errorSystem = `${system}\n\nERROR: ${error}\n\nPlease try again and ensure your response contains exactly one CSS code block inside \`\`\`css fences.`;
194
+
195
+ const { text: retryText } = await generateText({
196
+ model: createModel(props),
197
+ prompt: props.prompt,
198
+ system: errorSystem,
199
+ providerOptions: props.model?.options,
200
+ ...(props.temperature === undefined
201
+ ? {}
202
+ : { temperature: props.temperature }),
203
+ });
204
+
205
+ const retryResult = await extractCSSCode(retryText);
206
+
207
+ if (retryResult.error) {
208
+ throw new Error(
209
+ `Failed to generate valid CSS code: ${retryResult.error}\n${retryText}`,
210
+ );
211
+ }
212
+
213
+ code = retryResult.code;
214
+ }
215
+
216
+ if (this.phase === "update" && props.path !== this.props.path) {
217
+ await ignore("ENOENT", () => fs.unlink(this.props.path));
218
+ }
219
+
220
+ // Write content to file
221
+ await fs.writeFile(props.path, code);
222
+
223
+ // Get file stats for timestamps
224
+ const stats = await fs.stat(props.path);
225
+
226
+ // Return the resource
227
+ return this({
228
+ ...props,
229
+ content: code,
230
+ createdAt: stats.birthtimeMs,
231
+ updatedAt: stats.mtimeMs,
232
+ });
233
+ },
234
+ );
235
+
236
+ /**
237
+ * Extracts CSS code from between ```css fences
238
+ * Validates that exactly one CSS code block exists
239
+ *
240
+ * @param text The text to extract CSS code from
241
+ * @returns The extracted CSS code or error message
242
+ */
243
+ async function extractCSSCode(
244
+ text: string,
245
+ ): Promise<{ code: string; error?: string }> {
246
+ const cssCodeRegex = /```css\s*([\s\S]*?)```/g;
247
+ const matches = Array.from(text.matchAll(cssCodeRegex));
248
+
249
+ if (matches.length === 0) {
250
+ return {
251
+ code: "",
252
+ error:
253
+ "No CSS code block found in the response. Please include your code within ```css fences.",
254
+ };
255
+ }
256
+
257
+ if (matches.length > 1) {
258
+ return {
259
+ code: "",
260
+ error:
261
+ "Multiple CSS code blocks found in the response. Please provide exactly one code block within ```css fences.",
262
+ };
263
+ }
264
+
265
+ return { code: matches[0][1].trim() };
266
+ }
package/src/ai/data.ts CHANGED
@@ -4,12 +4,7 @@ import type { Context } from "../context";
4
4
  import { Resource } from "../resource";
5
5
  import type { Secret } from "../secret";
6
6
  import { ark } from "./ark";
7
- import {
8
- type ModelConfig,
9
- createClient,
10
- getModelId,
11
- getModelOptions,
12
- } from "./client";
7
+ import { type ModelConfig, createModel } from "./client";
13
8
 
14
9
  /**
15
10
  * Properties for creating or updating an AI Object
@@ -38,6 +33,14 @@ export interface DataProps<T extends Type<any, any>> {
38
33
  */
39
34
  system?: string;
40
35
 
36
+ /**
37
+ * Temperature for controlling randomness in generation.
38
+ * Higher values (e.g., 0.8) make output more random,
39
+ * lower values (e.g., 0.2) make it more deterministic.
40
+ * @default 0.7
41
+ */
42
+ temperature?: number;
43
+
41
44
  /**
42
45
  * Base URL for the OpenAI API
43
46
  * @default 'https://api.openai.com/v1'
@@ -86,13 +89,20 @@ export interface Data<T> extends Resource<"ai::Object"> {
86
89
  * price: "number"
87
90
  * });
88
91
  *
89
- * const product = await Object("new-product", {
92
+ * const product = await Data("new-product", {
90
93
  * schema: productSchema,
91
94
  * prompt: "Generate a product description for a new smartphone",
92
- * system: "You are a product copywriter specializing in tech products"
95
+ * system: "You are a product copywriter specializing in tech products",
96
+ * model: {
97
+ * id: "gpt-4o",
98
+ * provider: "openai",
99
+ * options: {
100
+ * temperature: 0.7
101
+ * }
102
+ * }
93
103
  * });
94
104
  *
95
- * console.log(product.content); // Typed as per schema
105
+ * console.log(product.object); // Typed as per schema
96
106
  *
97
107
  * @example
98
108
  * // Generate code documentation with context
@@ -106,13 +116,39 @@ export interface Data<T> extends Resource<"ai::Object"> {
106
116
  * returns: "string"
107
117
  * });
108
118
  *
109
- * const docs = await Object("function-docs", {
119
+ * const docs = await Data("function-docs", {
110
120
  * schema: docSchema,
111
121
  * prompt: await alchemy`
112
122
  * Generate documentation for this function:
113
123
  * ${alchemy.file("src/utils/format.ts")}
114
124
  * `,
115
- * system: "You are a technical documentation writer"
125
+ * system: "You are a technical documentation writer",
126
+ * temperature: 0.2
127
+ * });
128
+ *
129
+ * @example
130
+ * // Using specific model configuration with advanced options
131
+ * const analysisSchema = type({
132
+ * insights: "string[]",
133
+ * recommendations: "string[]",
134
+ * risk: "'low'|'medium'|'high'"
135
+ * });
136
+ *
137
+ * const analysis = await Data("code-analysis", {
138
+ * schema: analysisSchema,
139
+ * prompt: await alchemy`
140
+ * Analyze this code for security issues:
141
+ * ${alchemy.file("src/auth/login.ts")}
142
+ * `,
143
+ * system: "You are a security expert specializing in code analysis",
144
+ * model: {
145
+ * id: "o3-mini",
146
+ * provider: "openai",
147
+ * options: {
148
+ * reasoningEffort: "high"
149
+ * }
150
+ * },
151
+ * temperature: 0.1
116
152
  * });
117
153
  */
118
154
  export const Data = Resource("ai::Object", async function <
@@ -124,20 +160,21 @@ export const Data = Resource("ai::Object", async function <
124
160
  return this.destroy();
125
161
  }
126
162
 
127
- // Initialize OpenAI compatible provider using shared client
128
- const provider = createClient(props);
129
-
130
163
  // Generate structured output using generateObject
131
164
  const { object } = await generateObject({
132
- model: provider(getModelId(props)),
165
+ model: createModel(props),
133
166
  // Convert ArkType schema to Zod schema for generateObject
134
167
  // This is needed because generateObject expects a Zod schema
135
168
  schema: ark.schema<type.infer<T>>(props.schema),
169
+ providerOptions: props.model?.options,
136
170
  system:
137
171
  props.system ||
138
172
  "You are an AI assistant tasked with generating structured content.",
139
173
  prompt: props.prompt,
140
- ...getModelOptions(props),
174
+ ...(props.temperature === undefined
175
+ ? {}
176
+ : // some models error if you provide it (rather than ignoring it)
177
+ { temperature: props.temperature }),
141
178
  });
142
179
 
143
180
  // Return the resource with typed content
@@ -4,12 +4,8 @@ import path from "node:path";
4
4
  import type { Context } from "../context";
5
5
  import { Resource } from "../resource";
6
6
  import type { Secret } from "../secret";
7
- import {
8
- type ModelConfig,
9
- createClient,
10
- getModelId,
11
- getModelOptions,
12
- } from "./client";
7
+ import { ignore } from "../util/ignore";
8
+ import { type ModelConfig, createModel } from "./client";
13
9
 
14
10
  /**
15
11
  * Properties for creating or updating a Document
@@ -42,6 +38,14 @@ export interface DocumentProps {
42
38
  */
43
39
  prompt: string;
44
40
 
41
+ /**
42
+ * System prompt for the model
43
+ * This is used to provide instructions to the model about how to format the response
44
+ * The default system prompt instructs the model to return a single markdown document inside ```md fences
45
+ * @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."
46
+ */
47
+ system?: string;
48
+
45
49
  /**
46
50
  * OpenAI API key to use for generating content
47
51
  * If not provided, will use OPENAI_API_KEY environment variable
@@ -52,6 +56,14 @@ export interface DocumentProps {
52
56
  * Model configuration
53
57
  */
54
58
  model?: ModelConfig;
59
+
60
+ /**
61
+ * Temperature for controlling randomness in generation.
62
+ * Higher values (e.g., 0.8) make output more random,
63
+ * lower values (e.g., 0.2) make it more deterministic.
64
+ * @default 0.7
65
+ */
66
+ temperature?: number;
55
67
  }
56
68
 
57
69
  /**
@@ -74,6 +86,12 @@ export interface Document extends DocumentProps, Resource<"docs::Document"> {
74
86
  updatedAt: number;
75
87
  }
76
88
 
89
+ /**
90
+ * Default system prompt for markdown document generation
91
+ */
92
+ const DEFAULT_MD_SYSTEM_PROMPT =
93
+ "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.";
94
+
77
95
  /**
78
96
  * Resource for managing AI-generated markdown documents using the Vercel AI SDK.
79
97
  * Supports powerful context handling through the alchemy template literal tag.
@@ -81,39 +99,49 @@ export interface Document extends DocumentProps, Resource<"docs::Document"> {
81
99
  * @example
82
100
  * // Create a document using alchemy template literals for context
83
101
  * const apiDocs = await Document("api-docs", {
102
+ * title: "API Documentation",
84
103
  * path: "./docs/api.md",
85
104
  * prompt: await alchemy`
86
105
  * Generate API documentation based on these source files:
87
106
  * ${alchemy.file("src/api.ts")}
88
107
  * ${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
- * // ```
108
+ * `,
109
+ * model: {
110
+ * id: "gpt-4o",
111
+ * provider: "openai"
112
+ * }
105
113
  * });
106
114
  *
107
115
  * @example
108
- * // Use alchemy template literals with file collections
116
+ * // Use alchemy template literals with file collections and temperature control
109
117
  * const modelDocs = await Document("models", {
118
+ * title: "Data Models",
110
119
  * path: "./docs/models.md",
111
120
  * prompt: await alchemy`
112
121
  * Write documentation for these data models:
113
122
  * ${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
123
+ * `,
124
+ * temperature: 0.2 // Lower temperature for more deterministic output
125
+ * });
126
+ *
127
+ * @example
128
+ * // Advanced model configuration with custom provider options and custom system prompt
129
+ * const techDocs = await Document("tech-specs", {
130
+ * title: "Technical Specifications",
131
+ * path: "./docs/tech-specs.md",
132
+ * prompt: await alchemy`
133
+ * Create detailed technical specifications based on these requirements:
134
+ * ${alchemy.file("requirements/system.md")}
135
+ * `,
136
+ * system: "You are an expert technical writer specializing in system specifications. Create a single markdown document inside ```md fences with no additional text.",
137
+ * model: {
138
+ * id: "o3-mini",
139
+ * provider: "openai",
140
+ * options: {
141
+ * reasoningEffort: "high"
142
+ * }
143
+ * },
144
+ * temperature: 0.1
117
145
  * });
118
146
  */
119
147
  export const Document = Resource(
@@ -138,18 +166,55 @@ export const Document = Resource(
138
166
  return this.destroy();
139
167
  }
140
168
 
141
- // Initialize OpenAI compatible provider using shared client
142
- const provider = createClient(props);
169
+ // Use provided system prompt or default
170
+ const system = props.system || DEFAULT_MD_SYSTEM_PROMPT;
143
171
 
144
- // Generate content
172
+ // Generate initial content
145
173
  const { text } = await generateText({
146
- model: provider(getModelId(props)),
174
+ model: createModel(props),
147
175
  prompt: props.prompt,
148
- ...getModelOptions(props),
176
+ system,
177
+ providerOptions: props.model?.options,
178
+ ...(props.temperature === undefined
179
+ ? {}
180
+ : // some models error if you provide it (rather than ignoring it)
181
+ { temperature: props.temperature }),
149
182
  });
150
183
 
184
+ // Extract and validate markdown content
185
+ let { content, error } = await extractMarkdownContent(text);
186
+
187
+ // Re-prompt if there are validation errors
188
+ if (error) {
189
+ const errorSystem = `${system}\n\nERROR: ${error}\n\nPlease try again and ensure your response contains exactly one markdown document inside \`\`\`md fences.`;
190
+
191
+ const { text: retryText } = await generateText({
192
+ model: createModel(props),
193
+ prompt: props.prompt,
194
+ system: errorSystem,
195
+ providerOptions: props.model?.options,
196
+ ...(props.temperature === undefined
197
+ ? {}
198
+ : { temperature: props.temperature }),
199
+ });
200
+
201
+ const retryResult = await extractMarkdownContent(retryText);
202
+
203
+ if (retryResult.error) {
204
+ throw new Error(
205
+ `Failed to generate valid markdown content: ${retryResult.error}`,
206
+ );
207
+ }
208
+
209
+ content = retryResult.content;
210
+ }
211
+
212
+ if (this.phase === "update" && props.path !== this.props.path) {
213
+ await ignore("ENOENT", () => fs.unlink(this.props.path));
214
+ }
215
+
151
216
  // Write content to file
152
- await fs.writeFile(props.path, text);
217
+ await fs.writeFile(props.path, content);
153
218
 
154
219
  // Get file stats for timestamps
155
220
  const stats = await fs.stat(props.path);
@@ -157,9 +222,41 @@ export const Document = Resource(
157
222
  // Return the resource
158
223
  return this({
159
224
  ...props,
160
- content: text,
225
+ content: content,
161
226
  createdAt: stats.birthtimeMs,
162
227
  updatedAt: stats.mtimeMs,
163
228
  });
164
229
  },
165
230
  );
231
+
232
+ /**
233
+ * Extracts markdown content from between ```md fences
234
+ * Validates that exactly one markdown code block exists
235
+ *
236
+ * @param text The text to extract markdown content from
237
+ * @returns The extracted markdown content or error message
238
+ */
239
+ async function extractMarkdownContent(
240
+ text: string,
241
+ ): Promise<{ content: string; error?: string }> {
242
+ const mdCodeRegex = /```md\s*([\s\S]*?)```/g;
243
+ const matches = Array.from(text.matchAll(mdCodeRegex));
244
+
245
+ if (matches.length === 0) {
246
+ return {
247
+ content: "",
248
+ error:
249
+ "No markdown code block found in the response. Please include your markdown content within ```md fences.",
250
+ };
251
+ }
252
+
253
+ if (matches.length > 1) {
254
+ return {
255
+ content: "",
256
+ error:
257
+ "Multiple markdown code blocks found in the response. Please provide exactly one markdown block within ```md fences.",
258
+ };
259
+ }
260
+
261
+ return { content: matches[0][1].trim() };
262
+ }