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
@@ -0,0 +1,163 @@
1
+ import type { CoreMessage } from "ai";
2
+ import { type } from "arktype";
3
+ import { Data } from "./data";
4
+
5
+ /**
6
+ * Properties for approving or denying content
7
+ */
8
+ export interface ApproveProps {
9
+ /**
10
+ * Content to be reviewed
11
+ * Use alchemy template literals to include file context:
12
+ * @example
13
+ * content: await alchemy`
14
+ * Review this code:
15
+ * ${alchemy.file("src/api.ts")}
16
+ * `
17
+ *
18
+ * Required unless messages are provided
19
+ */
20
+ content?: string;
21
+
22
+ /**
23
+ * Prompt for the approval decision
24
+ * This should include specific criteria for approval or denial
25
+ * @example
26
+ * prompt: "Approve this code if it follows security best practices and has proper error handling."
27
+ *
28
+ * Required unless messages are provided
29
+ */
30
+ prompt?: string;
31
+
32
+ /**
33
+ * System prompt for the model
34
+ * This is used to provide instructions to the model about how to make the decision
35
+ */
36
+ system?: string;
37
+
38
+ /**
39
+ * Message history for the conversation
40
+ * If provided, this will be used instead of the prompt and content
41
+ */
42
+ messages?: CoreMessage[];
43
+
44
+ /**
45
+ * Temperature for controlling randomness in generation.
46
+ * Higher values (e.g., 0.8) make output more random,
47
+ * lower values (e.g., 0.2) make it more deterministic.
48
+ * @default 0.2 (lower for more consistent decisions)
49
+ */
50
+ temperature?: number;
51
+ }
52
+
53
+ /**
54
+ * Result of the approval process
55
+ */
56
+ export interface ApprovalResult {
57
+ /**
58
+ * Whether the content was approved
59
+ */
60
+ approved: boolean;
61
+
62
+ /**
63
+ * Explanation for the decision
64
+ */
65
+ explanation: string;
66
+
67
+ /**
68
+ * Suggestions for improvement if denied
69
+ */
70
+ suggestions?: string[];
71
+
72
+ /**
73
+ * Updated message history with the approval response appended
74
+ */
75
+ messages: CoreMessage[];
76
+ }
77
+
78
+ export type Approve = ApprovalResult;
79
+
80
+ /**
81
+ * Default system prompt for approval decisions
82
+ */
83
+ const DEFAULT_APPROVE_SYSTEM_PROMPT = `You are an expert content reviewer tasked with approving or denying content based on specific criteria.
84
+
85
+ Your role is to:
86
+ 1. Carefully evaluate the content against the provided criteria
87
+ 2. Make a clear decision to approve or deny the content
88
+ 3. Provide a detailed explanation for your decision
89
+ 4. If denying, offer specific suggestions for improvement
90
+
91
+ Be objective, thorough, and fair in your assessment.`;
92
+
93
+ /**
94
+ * Approves or denies content based on a prompt or message history
95
+ *
96
+ * @example
97
+ * // Approve or deny a code file
98
+ * const result = await Approve("code-approval", {
99
+ * content: await alchemy`
100
+ * Review this API implementation:
101
+ * ${alchemy.file("src/api.ts")}
102
+ * `,
103
+ * prompt: "Approve this code if it follows security best practices and has proper error handling."
104
+ * });
105
+ *
106
+ * if (result.approved) {
107
+ * console.log("Content approved:", result.explanation);
108
+ * } else {
109
+ * console.log("Content denied:", result.explanation);
110
+ * console.log("Suggestions:", result.suggestions);
111
+ * }
112
+ *
113
+ * @example
114
+ * // Approve or deny using message history
115
+ * const result = await Approve("doc-approval-with-history", {
116
+ * content: "This is the content to review",
117
+ * prompt: "Approve this documentation if it is clear and accurate",
118
+ * messages: [
119
+ * { role: "user", content: "Can you review this documentation?" },
120
+ * { role: "assistant", content: "Yes, I'd be happy to review it." },
121
+ * { role: "user", content: "Please check for clarity and accuracy." }
122
+ * ]
123
+ * });
124
+ */
125
+ export async function Approve(
126
+ id: string,
127
+ props: ApproveProps
128
+ ): Promise<ApprovalResult> {
129
+ // Create messages array if not provided
130
+ const messages =
131
+ props.messages ||
132
+ (props.content && props.prompt
133
+ ? [
134
+ {
135
+ role: "user" as const,
136
+ content: `${props.content}\n\n${props.prompt}`,
137
+ },
138
+ ]
139
+ : []);
140
+
141
+ if (messages.length === 0) {
142
+ throw new Error(
143
+ "Either messages or both content and prompt must be provided"
144
+ );
145
+ }
146
+
147
+ const data = await Data(id, {
148
+ schema: type({
149
+ approved: "boolean",
150
+ explanation: "string",
151
+ suggestions: "string[]?",
152
+ }),
153
+ messages,
154
+ system: props.system || DEFAULT_APPROVE_SYSTEM_PROMPT,
155
+ temperature: props.temperature ?? 0.2,
156
+ });
157
+
158
+ // Return the result with updated messages from Data
159
+ return {
160
+ ...data.object,
161
+ messages: data.messages,
162
+ };
163
+ }
@@ -0,0 +1,269 @@
1
+ import { generateText } from "ai";
2
+ import prettier from "prettier";
3
+ import type { Context } from "../context";
4
+ import { StaticAstroFile } from "../fs/static-astro-file";
5
+ import { Resource } from "../resource";
6
+ import type { Secret } from "../secret";
7
+ import { type ModelConfig, createModel } from "./client";
8
+
9
+ /**
10
+ * Properties for creating or updating an AstroFile
11
+ */
12
+ export interface AstroFileProps {
13
+ /**
14
+ * Path to the Astro file
15
+ */
16
+ path: string;
17
+
18
+ /**
19
+ * Base URL for the OpenAI API
20
+ * @default 'https://api.openai.com/v1'
21
+ */
22
+ baseURL?: string;
23
+
24
+ /**
25
+ * Prompt for generating content
26
+ * Use alchemy template literals to include file context:
27
+ * @example
28
+ * prompt: await alchemy`
29
+ * Generate an Astro component using:
30
+ * ${alchemy.file("src/types.ts")}
31
+ * `
32
+ */
33
+ prompt: string;
34
+
35
+ /**
36
+ * System prompt for the model
37
+ * This is used to provide instructions to the model about how to format the response
38
+ * The default system prompt instructs the model to return Astro code inside ```astro fences
39
+ * @default "You are an Astro component generator. Create Astro components based on the user's requirements. Your response MUST include only Astro code inside ```astro fences. Do not include any other text, explanations, or multiple code blocks."
40
+ */
41
+ system?: string;
42
+
43
+ /**
44
+ * OpenAI API key to use for generating content
45
+ * If not provided, will use OPENAI_API_KEY environment variable
46
+ */
47
+ apiKey?: Secret;
48
+
49
+ /**
50
+ * Model configuration
51
+ */
52
+ model?: ModelConfig;
53
+
54
+ /**
55
+ * Temperature for controlling randomness in generation.
56
+ * Higher values (e.g., 0.8) make output more random,
57
+ * lower values (e.g., 0.2) make it more deterministic.
58
+ * @default 0.7
59
+ */
60
+ temperature?: number;
61
+
62
+ /**
63
+ * Prettier configuration to use for formatting the Astro code
64
+ * If not provided, will use the default Prettier configuration
65
+ */
66
+ prettierConfig?: prettier.Options;
67
+ }
68
+
69
+ /**
70
+ * An Astro file that can be created, updated, and deleted
71
+ */
72
+ export interface AstroFile extends AstroFileProps, Resource<"ai::AstroFile"> {
73
+ /**
74
+ * Content of the Astro file
75
+ */
76
+ content: string;
77
+
78
+ /**
79
+ * Time at which the file was created
80
+ */
81
+ createdAt: number;
82
+
83
+ /**
84
+ * Time at which the file was last updated
85
+ */
86
+ updatedAt: number;
87
+ }
88
+
89
+ /**
90
+ * Default system prompt for Astro file generation
91
+ */
92
+ const DEFAULT_ASTRO_SYSTEM_PROMPT =
93
+ "You are an Astro component generator. Create Astro components based on the user's requirements. Your response MUST include only Astro code inside ```astro fences. Do not include any other text, explanations, or multiple code blocks.";
94
+
95
+ /**
96
+ * Resource for generating Astro files using AI models.
97
+ * Extracts Astro code from between ```astro fences, validates the response,
98
+ * and formats the code with Prettier.
99
+ *
100
+ * @example
101
+ * // Create a simple Astro component
102
+ * const header = await AstroFile("header", {
103
+ * path: "./src/components/Header.astro",
104
+ * prompt: await alchemy`
105
+ * Generate an Astro header component with:
106
+ * - Site logo
107
+ * - Navigation menu with Home, About, Services, Contact links
108
+ * - Mobile responsive design
109
+ * - Dark/light mode toggle
110
+ * `,
111
+ * model: {
112
+ * id: "gpt-4o",
113
+ * provider: "openai"
114
+ * }
115
+ * });
116
+ *
117
+ * @example
118
+ * // Generate an Astro page with data fetching
119
+ * const blogPost = await AstroFile("blog-post", {
120
+ * path: "./src/pages/blog/[slug].astro",
121
+ * prompt: await alchemy`
122
+ * Create an Astro blog post page that:
123
+ * - Uses getStaticPaths to generate pages from a CMS
124
+ * - Renders markdown content
125
+ * - Includes author info, publication date, and related posts
126
+ * - Has social sharing buttons
127
+ *
128
+ * Use the following types:
129
+ * ${alchemy.file("src/types/Blog.ts")}
130
+ * `,
131
+ * temperature: 0.2,
132
+ * prettierConfig: {
133
+ * semi: false,
134
+ * singleQuote: true,
135
+ * printWidth: 120
136
+ * }
137
+ * });
138
+ *
139
+ * @example
140
+ * // Generate a layout with custom system prompt
141
+ * const mainLayout = await AstroFile("main-layout", {
142
+ * path: "./src/layouts/MainLayout.astro",
143
+ * prompt: await alchemy`
144
+ * Create the main layout for an Astro site that:
145
+ * - Includes common head metadata and SEO optimization
146
+ * - Has slots for page content, header, and footer
147
+ * - Imports and uses the Header and Footer components
148
+ * - Sets up viewport and responsive configurations
149
+ * `,
150
+ * system: "You are an expert Astro developer. Create a single Astro layout file inside ```astro fences with no additional text. Follow Astro best practices and include proper typing in the frontmatter section.",
151
+ * model: {
152
+ * id: "claude-3-opus-20240229",
153
+ * provider: "anthropic"
154
+ * }
155
+ * });
156
+ */
157
+ export const AstroFile = Resource(
158
+ "ai::AstroFile",
159
+ async function (
160
+ this: Context<AstroFile>,
161
+ id: string,
162
+ props: AstroFileProps
163
+ ): Promise<AstroFile> {
164
+ // Handle deletion phase
165
+ if (this.phase === "delete") {
166
+ return this.destroy();
167
+ }
168
+
169
+ // Use provided system prompt or default
170
+ const system = props.system || DEFAULT_ASTRO_SYSTEM_PROMPT;
171
+
172
+ // Generate initial content
173
+ const { text } = await generateText({
174
+ model: createModel(props),
175
+ prompt: props.prompt,
176
+ system,
177
+ providerOptions: props.model?.options,
178
+ ...(props.temperature === undefined
179
+ ? {}
180
+ : { temperature: props.temperature }),
181
+ });
182
+
183
+ // Extract and validate Astro code
184
+ let { code, error } = await extractAstroCode(text);
185
+
186
+ // Re-prompt if there are validation errors
187
+ if (error) {
188
+ const errorSystem = `${system}\n\nERROR: ${error}\n\nPlease try again and ensure your response contains exactly one Astro code block inside \`\`\`astro fences.`;
189
+
190
+ const { text: retryText } = await generateText({
191
+ model: createModel(props),
192
+ prompt: props.prompt,
193
+ system: errorSystem,
194
+ providerOptions: props.model?.options,
195
+ ...(props.temperature === undefined
196
+ ? {}
197
+ : { temperature: props.temperature }),
198
+ });
199
+
200
+ const retryResult = await extractAstroCode(retryText);
201
+
202
+ if (retryResult.error) {
203
+ throw new Error(
204
+ `Failed to generate valid Astro code: ${retryResult.error}`
205
+ );
206
+ }
207
+
208
+ code = retryResult.code;
209
+ }
210
+
211
+ // Format the code with Prettier
212
+ try {
213
+ // Set default parser to astro
214
+ const prettierOptions: prettier.Options = {
215
+ parser: "astro",
216
+ ...props.prettierConfig,
217
+ };
218
+
219
+ // Format the code
220
+ code = await prettier.format(code, prettierOptions);
221
+ } catch (error) {
222
+ // If Prettier formatting fails, just use the unformatted code
223
+ console.warn("Failed to format Astro code with Prettier:", error);
224
+ }
225
+
226
+ // Use StaticAstroFile to create/update the file
227
+ const file = await StaticAstroFile("file", props.path, code);
228
+
229
+ // Return the resource
230
+ return this({
231
+ ...props,
232
+ content: file.content,
233
+ createdAt: Date.now(),
234
+ updatedAt: Date.now(),
235
+ });
236
+ }
237
+ );
238
+
239
+ /**
240
+ * Extracts Astro code from between ```astro fences
241
+ * Validates that exactly one Astro code block exists
242
+ *
243
+ * @param text The text to extract Astro code from
244
+ * @returns The extracted Astro code or error message
245
+ */
246
+ async function extractAstroCode(
247
+ text: string
248
+ ): Promise<{ code: string; error?: string }> {
249
+ const astroCodeRegex = /```astro\s*([\s\S]*?)```/g;
250
+ const matches = Array.from(text.matchAll(astroCodeRegex));
251
+
252
+ if (matches.length === 0) {
253
+ return {
254
+ code: "",
255
+ error:
256
+ "No Astro code block found in the response. Please include your code within ```astro fences.",
257
+ };
258
+ }
259
+
260
+ if (matches.length > 1) {
261
+ return {
262
+ code: "",
263
+ error:
264
+ "Multiple Astro code blocks found in the response. Please provide exactly one code block within ```astro fences.",
265
+ };
266
+ }
267
+
268
+ return { code: matches[0][1].trim() };
269
+ }
package/src/ai/client.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
1
+ import { anthropic } from "@ai-sdk/anthropic";
2
+ import { openai } from "@ai-sdk/openai";
2
3
  import type { Secret } from "../secret";
3
4
 
4
5
  /**
@@ -20,7 +21,7 @@ export interface ModelConfig {
20
21
  /**
21
22
  * Model-specific options
22
23
  */
23
- options?: Record<string, unknown>;
24
+ options?: Record<string, any>;
24
25
  }
25
26
 
26
27
  /**
@@ -48,31 +49,69 @@ export interface ClientConfig {
48
49
  /**
49
50
  * Creates an OpenAI-compatible client with the given configuration
50
51
  */
51
- export function createClient(config: ClientConfig) {
52
- // Get API key from props or environment
53
- const apiKey = config.apiKey?.unencrypted || process.env.OPENAI_API_KEY;
54
- if (!apiKey) {
55
- throw new Error("OpenAI API key is required");
52
+ export function createModel(config: ClientConfig) {
53
+ if (config.model?.provider === "anthropic") {
54
+ return anthropic(config.model?.id ?? "claude-3-7-sonnet-latest");
55
+ } else {
56
+ return openai(config.model?.id ?? "gpt-4o");
56
57
  }
57
-
58
- // Initialize OpenAI compatible provider
59
- return createOpenAICompatible({
60
- name: config.model?.provider || "openai",
61
- apiKey,
62
- baseURL: config.baseURL || "https://api.openai.com/v1",
63
- });
64
58
  }
65
59
 
66
60
  /**
67
- * Gets the model ID from the configuration or returns the default
61
+ * Maximum time to retry in milliseconds (5 minutes)
68
62
  */
69
- export function getModelId(config: ClientConfig): string {
70
- return config.model?.id || "gpt-4o";
71
- }
63
+ const MAX_RETRY_TIME = 5 * 60 * 1000;
64
+
65
+ /**
66
+ * Initial delay between retries in milliseconds
67
+ */
68
+ const INITIAL_RETRY_DELAY = 1000;
69
+
70
+ /**
71
+ * Maximum number of retries
72
+ */
73
+ const MAX_RETRIES = 10;
72
74
 
73
75
  /**
74
- * Gets the model options from the configuration
76
+ * Handles rate limiting with exponential backoff
77
+ * @param fn Function to retry
78
+ * @returns Result of the function
79
+ * @throws Error if max retries or time is exceeded
75
80
  */
76
- export function getModelOptions(config: ClientConfig): Record<string, unknown> {
77
- return config.model?.options || {};
81
+ export async function withRateLimitRetry<T>(fn: () => Promise<T>): Promise<T> {
82
+ let retryCount = 0;
83
+ let lastError: Error | null = null;
84
+ let startTime = Date.now();
85
+
86
+ while (true) {
87
+ try {
88
+ return await fn();
89
+ } catch (error: any) {
90
+ lastError = error;
91
+
92
+ console.log("retry error", error);
93
+
94
+ // Check if we should retry
95
+ const isRateLimit = error.statusCode === 429;
96
+ const timeElapsed = Date.now() - startTime;
97
+ const shouldRetry =
98
+ isRateLimit && retryCount < MAX_RETRIES && timeElapsed < MAX_RETRY_TIME;
99
+
100
+ if (!shouldRetry) {
101
+ throw error;
102
+ }
103
+
104
+ // Calculate delay with exponential backoff
105
+ const delay = Math.min(
106
+ INITIAL_RETRY_DELAY * Math.pow(2, retryCount),
107
+ MAX_RETRY_TIME - timeElapsed
108
+ );
109
+
110
+ console.log(`Retrying in ${delay}ms`);
111
+
112
+ // Wait before retrying
113
+ await new Promise((resolve) => setTimeout(resolve, delay));
114
+ retryCount++;
115
+ }
116
+ }
78
117
  }