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,263 @@
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 an HTMLFile
12
+ */
13
+ export interface HTMLFileProps {
14
+ /**
15
+ * Path to the HTML 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 an HTML page using:
31
+ * ${alchemy.file("src/templates/base.html")}
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 HTML code inside ```html fences
40
+ * @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."
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
+ * An HTML file that can be created, updated, and deleted
66
+ */
67
+ export interface HTMLFile extends HTMLFileProps, Resource<"ai::HTMLFile"> {
68
+ /**
69
+ * Content of the HTML 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 HTML file generation
86
+ */
87
+ const DEFAULT_HTML_SYSTEM_PROMPT =
88
+ "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.";
89
+
90
+ /**
91
+ * Resource for generating HTML files using AI models.
92
+ * Extracts HTML code from between ```html fences and validates the response.
93
+ *
94
+ * @example
95
+ * // Create a simple landing page
96
+ * const landingPage = await HTMLFile("landing-page", {
97
+ * path: "./public/index.html",
98
+ * prompt: await alchemy`
99
+ * Generate a modern landing page for a SaaS product with:
100
+ * - Hero section with headline and call-to-action
101
+ * - Features section with 3 key features
102
+ * - Pricing section with 3 tiers
103
+ * - Testimonials section with 2 customer quotes
104
+ * - Contact form and footer
105
+ * `,
106
+ * model: {
107
+ * id: "gpt-4o",
108
+ * provider: "openai"
109
+ * }
110
+ * });
111
+ *
112
+ * @example
113
+ * // Generate an HTML email template
114
+ * const emailTemplate = await HTMLFile("welcome-email", {
115
+ * path: "./emails/welcome.html",
116
+ * prompt: await alchemy`
117
+ * Create an HTML email template for welcoming new users to our platform.
118
+ * The email should include:
119
+ * - Company logo and branding
120
+ * - Personalized welcome message (use {{name}} placeholder)
121
+ * - Three steps to get started
122
+ * - Support contact information
123
+ * - Unsubscribe footer
124
+ *
125
+ * Make sure it's responsive and works in all major email clients.
126
+ * `,
127
+ * temperature: 0.2
128
+ * });
129
+ *
130
+ * @example
131
+ * // Generate an HTML component with custom system prompt
132
+ * const navComponent = await HTMLFile("navigation", {
133
+ * path: "./components/nav.html",
134
+ * prompt: await alchemy`
135
+ * Create a responsive navigation component with:
136
+ * - Logo in the left corner
137
+ * - Navigation links: Home, Products, Services, About, Contact
138
+ * - Mobile hamburger menu that expands/collapses
139
+ * - Login/signup buttons on the right side
140
+ * - Dark/light mode toggle
141
+ * `,
142
+ * 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.",
143
+ * model: {
144
+ * id: "claude-3-opus-20240229",
145
+ * provider: "anthropic"
146
+ * }
147
+ * });
148
+ */
149
+ export const HTMLFile = Resource(
150
+ "ai::HTMLFile",
151
+ async function (
152
+ this: Context<HTMLFile>,
153
+ id: string,
154
+ props: HTMLFileProps,
155
+ ): Promise<HTMLFile> {
156
+ // Ensure directory exists
157
+ await fs.mkdir(path.dirname(props.path), { recursive: true });
158
+
159
+ if (this.phase === "delete") {
160
+ try {
161
+ await fs.unlink(props.path);
162
+ } catch (error: any) {
163
+ // Ignore if file doesn't exist
164
+ if (error.code !== "ENOENT") {
165
+ throw error;
166
+ }
167
+ }
168
+ return this.destroy();
169
+ }
170
+
171
+ // Use provided system prompt or default
172
+ const system = props.system || DEFAULT_HTML_SYSTEM_PROMPT;
173
+
174
+ // Generate initial content
175
+ const { text } = await generateText({
176
+ model: createModel(props),
177
+ prompt: props.prompt,
178
+ system,
179
+ providerOptions: props.model?.options,
180
+ ...(props.temperature === undefined
181
+ ? {}
182
+ : { temperature: props.temperature }),
183
+ });
184
+
185
+ // Extract and validate HTML code
186
+ let { code, error } = await extractHTMLCode(text);
187
+
188
+ // Re-prompt if there are validation errors
189
+ if (error) {
190
+ const errorSystem = `${system}\n\nERROR: ${error}\n\nPlease try again and ensure your response contains exactly one HTML code block inside \`\`\`html fences.`;
191
+
192
+ const { text: retryText } = await generateText({
193
+ model: createModel(props),
194
+ prompt: props.prompt,
195
+ system: errorSystem,
196
+ providerOptions: props.model?.options,
197
+ ...(props.temperature === undefined
198
+ ? {}
199
+ : { temperature: props.temperature }),
200
+ });
201
+
202
+ const retryResult = await extractHTMLCode(retryText);
203
+
204
+ if (retryResult.error) {
205
+ throw new Error(
206
+ `Failed to generate valid HTML code: ${retryResult.error}`,
207
+ );
208
+ }
209
+
210
+ code = retryResult.code;
211
+ }
212
+
213
+ if (this.phase === "update" && props.path !== this.props.path) {
214
+ await ignore("ENOENT", () => fs.unlink(this.props.path));
215
+ }
216
+
217
+ // Write content to file
218
+ await fs.writeFile(props.path, code);
219
+
220
+ // Get file stats for timestamps
221
+ const stats = await fs.stat(props.path);
222
+
223
+ // Return the resource
224
+ return this({
225
+ ...props,
226
+ content: code,
227
+ createdAt: stats.birthtimeMs,
228
+ updatedAt: stats.mtimeMs,
229
+ });
230
+ },
231
+ );
232
+
233
+ /**
234
+ * Extracts HTML code from between ```html fences
235
+ * Validates that exactly one HTML code block exists
236
+ *
237
+ * @param text The text to extract HTML code from
238
+ * @returns The extracted HTML code or error message
239
+ */
240
+ async function extractHTMLCode(
241
+ text: string,
242
+ ): Promise<{ code: string; error?: string }> {
243
+ const htmlCodeRegex = /```html\s*([\s\S]*?)```/g;
244
+ const matches = Array.from(text.matchAll(htmlCodeRegex));
245
+
246
+ if (matches.length === 0) {
247
+ return {
248
+ code: "",
249
+ error:
250
+ "No HTML code block found in the response. Please include your code within ```html fences.",
251
+ };
252
+ }
253
+
254
+ if (matches.length > 1) {
255
+ return {
256
+ code: "",
257
+ error:
258
+ "Multiple HTML code blocks found in the response. Please provide exactly one code block within ```html fences.",
259
+ };
260
+ }
261
+
262
+ return { code: matches[0][1].trim() };
263
+ }
package/src/ai/index.ts CHANGED
@@ -1,3 +1,10 @@
1
1
  export * from "./ark";
2
+ export * from "./astro-file";
3
+ export * from "./css-file";
2
4
  export * from "./data";
3
5
  export * from "./document";
6
+ export * from "./html-file";
7
+ export * from "./json-file";
8
+ export * from "./typescript-file";
9
+ export * from "./vue-file";
10
+ export * from "./yaml-file";
@@ -0,0 +1,349 @@
1
+ import { generateObject, generateText } from "ai";
2
+ import type { JsonSchema, Type, type } from "arktype";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import type { Context } from "../context";
6
+ import { Resource } from "../resource";
7
+ import type { Secret } from "../secret";
8
+ import { ignore } from "../util/ignore";
9
+ import { ark } from "./ark";
10
+ import { type ModelConfig, createModel } from "./client";
11
+
12
+ /**
13
+ * Properties for creating or updating a JSONFile
14
+ */
15
+ export interface JSONFileProps<
16
+ T extends Type<any, any> | undefined = undefined,
17
+ > {
18
+ /**
19
+ * Path to the JSON file
20
+ */
21
+ path: string;
22
+
23
+ /**
24
+ * Optional ArkType schema to validate and structure the generated JSON
25
+ * When provided, the resource will use generateObject with schema validation
26
+ * When not provided, it will extract JSON from between ```json fences
27
+ */
28
+ schema?: T;
29
+
30
+ /**
31
+ * Base URL for the OpenAI API
32
+ * @default 'https://api.openai.com/v1'
33
+ */
34
+ baseURL?: string;
35
+
36
+ /**
37
+ * Prompt for generating content
38
+ * Use alchemy template literals to include file context:
39
+ * @example
40
+ * prompt: await alchemy`
41
+ * Generate a JSON configuration for:
42
+ * ${alchemy.file("src/config.ts")}
43
+ * `
44
+ */
45
+ prompt: string;
46
+
47
+ /**
48
+ * System prompt for the model
49
+ * This is used to provide instructions to the model about how to format the response
50
+ * @default Depends on whether schema is provided
51
+ */
52
+ system?: string;
53
+
54
+ /**
55
+ * OpenAI API key to use for generating content
56
+ * If not provided, will use OPENAI_API_KEY environment variable
57
+ */
58
+ apiKey?: Secret;
59
+
60
+ /**
61
+ * Model configuration
62
+ */
63
+ model?: ModelConfig;
64
+
65
+ /**
66
+ * Temperature for controlling randomness in generation.
67
+ * Higher values (e.g., 0.8) make output more random,
68
+ * lower values (e.g., 0.2) make it more deterministic.
69
+ * @default 0.7
70
+ */
71
+ temperature?: number;
72
+
73
+ /**
74
+ * Whether to pretty-print the JSON with indentation
75
+ * @default true
76
+ */
77
+ pretty?: boolean;
78
+
79
+ /**
80
+ * Number of spaces to use for indentation when pretty-printing
81
+ * @default 2
82
+ */
83
+ indent?: number;
84
+ }
85
+
86
+ /**
87
+ * A JSON file that can be created, updated, and deleted
88
+ */
89
+ export interface JSONFile<T = any>
90
+ extends Omit<JSONFileProps, "schema">,
91
+ Resource<"ai::JSONFile"> {
92
+ /**
93
+ * Content of the JSON file as a string
94
+ */
95
+ content: string;
96
+
97
+ /**
98
+ * Parsed JSON object
99
+ */
100
+ json: T;
101
+
102
+ /**
103
+ * Schema used to validate the JSON (if provided)
104
+ */
105
+ schema?: JsonSchema;
106
+
107
+ /**
108
+ * Time at which the file was created
109
+ */
110
+ createdAt: number;
111
+
112
+ /**
113
+ * Time at which the file was last updated
114
+ */
115
+ updatedAt: number;
116
+ }
117
+
118
+ /**
119
+ * Default system prompt for JSON file generation without schema
120
+ */
121
+ const DEFAULT_JSON_SYSTEM_PROMPT =
122
+ "You are a JSON generator. Create valid JSON based on the user's requirements. Your response MUST include only JSON inside ```json fences. Do not include any other text, explanations, or multiple code blocks.";
123
+
124
+ /**
125
+ * Resource for generating JSON files using AI models.
126
+ * Can operate in two modes:
127
+ * 1. With schema: Uses generateObject with type validation
128
+ * 2. Without schema: Extracts JSON from between ```json fences
129
+ *
130
+ * @example
131
+ * // Generate a configuration file with freeform JSON
132
+ * const config = await JSONFile("app-config", {
133
+ * path: "./config/app.json",
134
+ * prompt: await alchemy`
135
+ * Generate a configuration for a web application with:
136
+ * - Server settings (port, host, timeout)
137
+ * - Database connection details (redact any passwords)
138
+ * - Logging configuration
139
+ * - Feature flags
140
+ * `,
141
+ * model: {
142
+ * id: "gpt-4o",
143
+ * provider: "openai"
144
+ * }
145
+ * });
146
+ *
147
+ * @example
148
+ * // Generate JSON with schema validation
149
+ * import { type } from "arktype";
150
+ *
151
+ * const userSchema = type({
152
+ * users: [{
153
+ * id: "string",
154
+ * name: "string",
155
+ * email: "string",
156
+ * role: "'admin' | 'user' | 'guest'",
157
+ * permissions: "string[]",
158
+ * active: "boolean"
159
+ * }]
160
+ * });
161
+ *
162
+ * const userData = await JSONFile("user-data", {
163
+ * path: "./data/users.json",
164
+ * schema: userSchema,
165
+ * prompt: "Generate sample user data for an application with various roles and permissions",
166
+ * temperature: 0.2
167
+ * });
168
+ *
169
+ * // Type-safe access to the generated data
170
+ * console.log(userData.json.users[0].role); // Typed as 'admin' | 'user' | 'guest'
171
+ *
172
+ * @example
173
+ * // Generate API mock data with custom system prompt
174
+ * const apiMock = await JSONFile("api-mock", {
175
+ * path: "./mocks/products-api.json",
176
+ * prompt: await alchemy`
177
+ * Create mock data for a product catalog API response with:
178
+ * - 10 products with different categories
179
+ * - Each product should have id, name, price, category, inventory, and image_url
180
+ * - Include pagination metadata (total, page, limit)
181
+ * `,
182
+ * system: "You are an API design expert. Create realistic mock JSON data that follows REST API best practices. Your response must be valid JSON inside ```json fences.",
183
+ * model: {
184
+ * id: "claude-3-opus-20240229",
185
+ * provider: "anthropic"
186
+ * },
187
+ * pretty: true,
188
+ * indent: 4
189
+ * });
190
+ */
191
+ export const JSONFile = Resource("ai::JSONFile", async function <
192
+ const T extends Type<any, any> | undefined = undefined,
193
+ >(this: Context<JSONFile<T extends Type<any, any> ? type.infer<T> : any>>, id: string, props: JSONFileProps<T>): Promise<
194
+ JSONFile<T extends Type<any, any> ? type.infer<T> : any>
195
+ > {
196
+ // Ensure directory exists
197
+ await fs.mkdir(path.dirname(props.path), { recursive: true });
198
+
199
+ if (this.phase === "delete") {
200
+ try {
201
+ await fs.unlink(props.path);
202
+ } catch (error: any) {
203
+ // Ignore if file doesn't exist
204
+ if (error.code !== "ENOENT") {
205
+ throw error;
206
+ }
207
+ }
208
+ return this.destroy();
209
+ }
210
+
211
+ // Determine if we should use pretty printing
212
+ const pretty = props.pretty !== false;
213
+ const indent = props.indent ?? 2;
214
+
215
+ let jsonContent: string;
216
+ let jsonObject: any;
217
+
218
+ // Check if schema is provided
219
+ if (props.schema) {
220
+ // Use schema-based generation
221
+ const { object } = await generateObject({
222
+ model: createModel(props),
223
+ schema: ark.schema<type.infer<typeof props.schema>>(props.schema),
224
+ providerOptions: props.model?.options,
225
+ system:
226
+ props.system ||
227
+ "Generate a valid JSON object based on the provided requirements.",
228
+ prompt: props.prompt,
229
+ ...(props.temperature === undefined
230
+ ? {}
231
+ : { temperature: props.temperature }),
232
+ });
233
+
234
+ jsonObject = object;
235
+ jsonContent = pretty
236
+ ? JSON.stringify(jsonObject, null, indent)
237
+ : JSON.stringify(jsonObject);
238
+ } else {
239
+ // Use fence-based extraction
240
+ // Use provided system prompt or default
241
+ const system = props.system || DEFAULT_JSON_SYSTEM_PROMPT;
242
+
243
+ // Generate initial content
244
+ const { text } = await generateText({
245
+ model: createModel(props),
246
+ prompt: props.prompt,
247
+ system,
248
+ providerOptions: props.model?.options,
249
+ ...(props.temperature === undefined
250
+ ? {}
251
+ : { temperature: props.temperature }),
252
+ });
253
+
254
+ // Extract and validate JSON content
255
+ let { content, error } = await extractJSONContent(text);
256
+
257
+ // Re-prompt if there are validation errors
258
+ if (error) {
259
+ const errorSystem = `${system}\n\nERROR: ${error}\n\nPlease try again and ensure your response contains exactly one valid JSON block inside \`\`\`json fences.`;
260
+
261
+ const { text: retryText } = await generateText({
262
+ model: createModel(props),
263
+ prompt: props.prompt,
264
+ system: errorSystem,
265
+ providerOptions: props.model?.options,
266
+ ...(props.temperature === undefined
267
+ ? {}
268
+ : { temperature: props.temperature }),
269
+ });
270
+
271
+ const retryResult = await extractJSONContent(retryText);
272
+
273
+ if (retryResult.error) {
274
+ throw new Error(`Failed to generate valid JSON: ${retryResult.error}`);
275
+ }
276
+
277
+ content = retryResult.content;
278
+ }
279
+
280
+ // Parse JSON
281
+ jsonObject = JSON.parse(content);
282
+
283
+ // Format with or without indentation based on pretty option
284
+ jsonContent = pretty ? JSON.stringify(jsonObject, null, indent) : content;
285
+ }
286
+
287
+ if (this.phase === "update" && props.path !== this.props.path) {
288
+ await ignore("ENOENT", () => fs.unlink(this.props.path));
289
+ }
290
+
291
+ // Write content to file
292
+ await fs.writeFile(props.path, jsonContent);
293
+
294
+ // Get file stats for timestamps
295
+ const stats = await fs.stat(props.path);
296
+
297
+ // Return the resource
298
+ return this({
299
+ ...props,
300
+ schema: props.schema,
301
+ content: jsonContent,
302
+ json: jsonObject,
303
+ createdAt: stats.birthtimeMs,
304
+ updatedAt: stats.mtimeMs,
305
+ });
306
+ });
307
+
308
+ /**
309
+ * Extracts JSON content from between ```json fences
310
+ * Validates that exactly one JSON code block exists
311
+ *
312
+ * @param text The text to extract JSON from
313
+ * @returns The extracted JSON or error message
314
+ */
315
+ async function extractJSONContent(
316
+ text: string,
317
+ ): Promise<{ content: string; error?: string }> {
318
+ const jsonCodeRegex = /```json\s*([\s\S]*?)```/g;
319
+ const matches = Array.from(text.matchAll(jsonCodeRegex));
320
+
321
+ if (matches.length === 0) {
322
+ return {
323
+ content: "",
324
+ error:
325
+ "No JSON code block found in the response. Please include your JSON within ```json fences.",
326
+ };
327
+ }
328
+
329
+ if (matches.length > 1) {
330
+ return {
331
+ content: "",
332
+ error:
333
+ "Multiple JSON code blocks found in the response. Please provide exactly one JSON block within ```json fences.",
334
+ };
335
+ }
336
+
337
+ const content = matches[0][1].trim();
338
+
339
+ // Validate JSON can be parsed
340
+ try {
341
+ JSON.parse(content);
342
+ return { content };
343
+ } catch (e) {
344
+ return {
345
+ content: "",
346
+ error: `Invalid JSON: ${(e as Error).message}. Please provide valid JSON syntax.`,
347
+ };
348
+ }
349
+ }