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
package/README.md CHANGED
@@ -9,7 +9,7 @@ Resources are simple memoized async functions that can run in any JavaScript run
9
9
  ```ts
10
10
  import alchemy from "alchemy";
11
11
 
12
- await using _ = alchemy("cloudflare-worker");
12
+ await using app = alchemy("cloudflare-worker");
13
13
 
14
14
  export const worker = await Worker("worker", {
15
15
  name: "my-worker",
@@ -38,8 +38,8 @@ export const worker = await Worker("worker", {
38
38
 
39
39
  # Examples
40
40
 
41
- - CloudFlare ViteJS Website + API Backend with Durable Objects: [examples/cloudflare-vite/](./examples/cloudflare-vite/alchemy.config.ts)
42
- - Deploy an AWS Lambda Function with a DynamoDB Table and IAM Role: [examples/aws-app/](./examples/aws-app/alchemy.config.ts)
41
+ - CloudFlare ViteJS Website + API Backend with Durable Objects: [examples/cloudflare-vite/](./examples/cloudflare-vite/alchemy.run.ts)
42
+ - Deploy an AWS Lambda Function with a DynamoDB Table and IAM Role: [examples/aws-app/](./examples/aws-app/alchemy.run.ts)
43
43
 
44
44
  # Getting Started
45
45
 
@@ -0,0 +1,99 @@
1
+ import type { CoreMessage } from "ai";
2
+ /**
3
+ * Properties for approving or denying content
4
+ */
5
+ export interface ApproveProps {
6
+ /**
7
+ * Content to be reviewed
8
+ * Use alchemy template literals to include file context:
9
+ * @example
10
+ * content: await alchemy`
11
+ * Review this code:
12
+ * ${alchemy.file("src/api.ts")}
13
+ * `
14
+ *
15
+ * Required unless messages are provided
16
+ */
17
+ content?: string;
18
+ /**
19
+ * Prompt for the approval decision
20
+ * This should include specific criteria for approval or denial
21
+ * @example
22
+ * prompt: "Approve this code if it follows security best practices and has proper error handling."
23
+ *
24
+ * Required unless messages are provided
25
+ */
26
+ prompt?: string;
27
+ /**
28
+ * System prompt for the model
29
+ * This is used to provide instructions to the model about how to make the decision
30
+ */
31
+ system?: string;
32
+ /**
33
+ * Message history for the conversation
34
+ * If provided, this will be used instead of the prompt and content
35
+ */
36
+ messages?: CoreMessage[];
37
+ /**
38
+ * Temperature for controlling randomness in generation.
39
+ * Higher values (e.g., 0.8) make output more random,
40
+ * lower values (e.g., 0.2) make it more deterministic.
41
+ * @default 0.2 (lower for more consistent decisions)
42
+ */
43
+ temperature?: number;
44
+ }
45
+ /**
46
+ * Result of the approval process
47
+ */
48
+ export interface ApprovalResult {
49
+ /**
50
+ * Whether the content was approved
51
+ */
52
+ approved: boolean;
53
+ /**
54
+ * Explanation for the decision
55
+ */
56
+ explanation: string;
57
+ /**
58
+ * Suggestions for improvement if denied
59
+ */
60
+ suggestions?: string[];
61
+ /**
62
+ * Updated message history with the approval response appended
63
+ */
64
+ messages: CoreMessage[];
65
+ }
66
+ export type Approve = ApprovalResult;
67
+ /**
68
+ * Approves or denies content based on a prompt or message history
69
+ *
70
+ * @example
71
+ * // Approve or deny a code file
72
+ * const result = await Approve("code-approval", {
73
+ * content: await alchemy`
74
+ * Review this API implementation:
75
+ * ${alchemy.file("src/api.ts")}
76
+ * `,
77
+ * prompt: "Approve this code if it follows security best practices and has proper error handling."
78
+ * });
79
+ *
80
+ * if (result.approved) {
81
+ * console.log("Content approved:", result.explanation);
82
+ * } else {
83
+ * console.log("Content denied:", result.explanation);
84
+ * console.log("Suggestions:", result.suggestions);
85
+ * }
86
+ *
87
+ * @example
88
+ * // Approve or deny using message history
89
+ * const result = await Approve("doc-approval-with-history", {
90
+ * content: "This is the content to review",
91
+ * prompt: "Approve this documentation if it is clear and accurate",
92
+ * messages: [
93
+ * { role: "user", content: "Can you review this documentation?" },
94
+ * { role: "assistant", content: "Yes, I'd be happy to review it." },
95
+ * { role: "user", content: "Please check for clarity and accuracy." }
96
+ * ]
97
+ * });
98
+ */
99
+ export declare function Approve(id: string, props: ApproveProps): Promise<ApprovalResult>;
@@ -0,0 +1,76 @@
1
+ import { type } from "arktype";
2
+ import { Data } from "./data";
3
+ /**
4
+ * Default system prompt for approval decisions
5
+ */
6
+ const DEFAULT_APPROVE_SYSTEM_PROMPT = `You are an expert content reviewer tasked with approving or denying content based on specific criteria.
7
+
8
+ Your role is to:
9
+ 1. Carefully evaluate the content against the provided criteria
10
+ 2. Make a clear decision to approve or deny the content
11
+ 3. Provide a detailed explanation for your decision
12
+ 4. If denying, offer specific suggestions for improvement
13
+
14
+ Be objective, thorough, and fair in your assessment.`;
15
+ /**
16
+ * Approves or denies content based on a prompt or message history
17
+ *
18
+ * @example
19
+ * // Approve or deny a code file
20
+ * const result = await Approve("code-approval", {
21
+ * content: await alchemy`
22
+ * Review this API implementation:
23
+ * ${alchemy.file("src/api.ts")}
24
+ * `,
25
+ * prompt: "Approve this code if it follows security best practices and has proper error handling."
26
+ * });
27
+ *
28
+ * if (result.approved) {
29
+ * console.log("Content approved:", result.explanation);
30
+ * } else {
31
+ * console.log("Content denied:", result.explanation);
32
+ * console.log("Suggestions:", result.suggestions);
33
+ * }
34
+ *
35
+ * @example
36
+ * // Approve or deny using message history
37
+ * const result = await Approve("doc-approval-with-history", {
38
+ * content: "This is the content to review",
39
+ * prompt: "Approve this documentation if it is clear and accurate",
40
+ * messages: [
41
+ * { role: "user", content: "Can you review this documentation?" },
42
+ * { role: "assistant", content: "Yes, I'd be happy to review it." },
43
+ * { role: "user", content: "Please check for clarity and accuracy." }
44
+ * ]
45
+ * });
46
+ */
47
+ export async function Approve(id, props) {
48
+ // Create messages array if not provided
49
+ const messages = props.messages ||
50
+ (props.content && props.prompt
51
+ ? [
52
+ {
53
+ role: "user",
54
+ content: `${props.content}\n\n${props.prompt}`,
55
+ },
56
+ ]
57
+ : []);
58
+ if (messages.length === 0) {
59
+ throw new Error("Either messages or both content and prompt must be provided");
60
+ }
61
+ const data = await Data(id, {
62
+ schema: type({
63
+ approved: "boolean",
64
+ explanation: "string",
65
+ suggestions: "string[]?",
66
+ }),
67
+ messages,
68
+ system: props.system || DEFAULT_APPROVE_SYSTEM_PROMPT,
69
+ temperature: props.temperature ?? 0.2,
70
+ });
71
+ // Return the result with updated messages from Data
72
+ return {
73
+ ...data.object,
74
+ messages: data.messages,
75
+ };
76
+ }
@@ -0,0 +1,137 @@
1
+ import prettier from "prettier";
2
+ import type { Context } from "../context";
3
+ import { Resource } from "../resource";
4
+ import type { Secret } from "../secret";
5
+ import { type ModelConfig } from "./client";
6
+ /**
7
+ * Properties for creating or updating an AstroFile
8
+ */
9
+ export interface AstroFileProps {
10
+ /**
11
+ * Path to the Astro file
12
+ */
13
+ path: string;
14
+ /**
15
+ * Base URL for the OpenAI API
16
+ * @default 'https://api.openai.com/v1'
17
+ */
18
+ baseURL?: string;
19
+ /**
20
+ * Prompt for generating content
21
+ * Use alchemy template literals to include file context:
22
+ * @example
23
+ * prompt: await alchemy`
24
+ * Generate an Astro component using:
25
+ * ${alchemy.file("src/types.ts")}
26
+ * `
27
+ */
28
+ prompt: string;
29
+ /**
30
+ * System prompt for the model
31
+ * This is used to provide instructions to the model about how to format the response
32
+ * The default system prompt instructs the model to return Astro code inside ```astro fences
33
+ * @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."
34
+ */
35
+ system?: string;
36
+ /**
37
+ * OpenAI API key to use for generating content
38
+ * If not provided, will use OPENAI_API_KEY environment variable
39
+ */
40
+ apiKey?: Secret;
41
+ /**
42
+ * Model configuration
43
+ */
44
+ model?: ModelConfig;
45
+ /**
46
+ * Temperature for controlling randomness in generation.
47
+ * Higher values (e.g., 0.8) make output more random,
48
+ * lower values (e.g., 0.2) make it more deterministic.
49
+ * @default 0.7
50
+ */
51
+ temperature?: number;
52
+ /**
53
+ * Prettier configuration to use for formatting the Astro code
54
+ * If not provided, will use the default Prettier configuration
55
+ */
56
+ prettierConfig?: prettier.Options;
57
+ }
58
+ /**
59
+ * An Astro file that can be created, updated, and deleted
60
+ */
61
+ export interface AstroFile extends AstroFileProps, Resource<"ai::AstroFile"> {
62
+ /**
63
+ * Content of the Astro file
64
+ */
65
+ content: string;
66
+ /**
67
+ * Time at which the file was created
68
+ */
69
+ createdAt: number;
70
+ /**
71
+ * Time at which the file was last updated
72
+ */
73
+ updatedAt: number;
74
+ }
75
+ /**
76
+ * Resource for generating Astro files using AI models.
77
+ * Extracts Astro code from between ```astro fences, validates the response,
78
+ * and formats the code with Prettier.
79
+ *
80
+ * @example
81
+ * // Create a simple Astro component
82
+ * const header = await AstroFile("header", {
83
+ * path: "./src/components/Header.astro",
84
+ * prompt: await alchemy`
85
+ * Generate an Astro header component with:
86
+ * - Site logo
87
+ * - Navigation menu with Home, About, Services, Contact links
88
+ * - Mobile responsive design
89
+ * - Dark/light mode toggle
90
+ * `,
91
+ * model: {
92
+ * id: "gpt-4o",
93
+ * provider: "openai"
94
+ * }
95
+ * });
96
+ *
97
+ * @example
98
+ * // Generate an Astro page with data fetching
99
+ * const blogPost = await AstroFile("blog-post", {
100
+ * path: "./src/pages/blog/[slug].astro",
101
+ * prompt: await alchemy`
102
+ * Create an Astro blog post page that:
103
+ * - Uses getStaticPaths to generate pages from a CMS
104
+ * - Renders markdown content
105
+ * - Includes author info, publication date, and related posts
106
+ * - Has social sharing buttons
107
+ *
108
+ * Use the following types:
109
+ * ${alchemy.file("src/types/Blog.ts")}
110
+ * `,
111
+ * temperature: 0.2,
112
+ * prettierConfig: {
113
+ * semi: false,
114
+ * singleQuote: true,
115
+ * printWidth: 120
116
+ * }
117
+ * });
118
+ *
119
+ * @example
120
+ * // Generate a layout with custom system prompt
121
+ * const mainLayout = await AstroFile("main-layout", {
122
+ * path: "./src/layouts/MainLayout.astro",
123
+ * prompt: await alchemy`
124
+ * Create the main layout for an Astro site that:
125
+ * - Includes common head metadata and SEO optimization
126
+ * - Has slots for page content, header, and footer
127
+ * - Imports and uses the Header and Footer components
128
+ * - Sets up viewport and responsive configurations
129
+ * `,
130
+ * 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.",
131
+ * model: {
132
+ * id: "claude-3-opus-20240229",
133
+ * provider: "anthropic"
134
+ * }
135
+ * });
136
+ */
137
+ export declare const AstroFile: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<AstroFile>, id: string, props: AstroFileProps) => Promise<AstroFile>);
@@ -0,0 +1,156 @@
1
+ import { generateText } from "ai";
2
+ import prettier from "prettier";
3
+ import { StaticAstroFile } from "../fs/static-astro-file";
4
+ import { Resource } from "../resource";
5
+ import { createModel } from "./client";
6
+ /**
7
+ * Default system prompt for Astro file generation
8
+ */
9
+ const DEFAULT_ASTRO_SYSTEM_PROMPT = "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.";
10
+ /**
11
+ * Resource for generating Astro files using AI models.
12
+ * Extracts Astro code from between ```astro fences, validates the response,
13
+ * and formats the code with Prettier.
14
+ *
15
+ * @example
16
+ * // Create a simple Astro component
17
+ * const header = await AstroFile("header", {
18
+ * path: "./src/components/Header.astro",
19
+ * prompt: await alchemy`
20
+ * Generate an Astro header component with:
21
+ * - Site logo
22
+ * - Navigation menu with Home, About, Services, Contact links
23
+ * - Mobile responsive design
24
+ * - Dark/light mode toggle
25
+ * `,
26
+ * model: {
27
+ * id: "gpt-4o",
28
+ * provider: "openai"
29
+ * }
30
+ * });
31
+ *
32
+ * @example
33
+ * // Generate an Astro page with data fetching
34
+ * const blogPost = await AstroFile("blog-post", {
35
+ * path: "./src/pages/blog/[slug].astro",
36
+ * prompt: await alchemy`
37
+ * Create an Astro blog post page that:
38
+ * - Uses getStaticPaths to generate pages from a CMS
39
+ * - Renders markdown content
40
+ * - Includes author info, publication date, and related posts
41
+ * - Has social sharing buttons
42
+ *
43
+ * Use the following types:
44
+ * ${alchemy.file("src/types/Blog.ts")}
45
+ * `,
46
+ * temperature: 0.2,
47
+ * prettierConfig: {
48
+ * semi: false,
49
+ * singleQuote: true,
50
+ * printWidth: 120
51
+ * }
52
+ * });
53
+ *
54
+ * @example
55
+ * // Generate a layout with custom system prompt
56
+ * const mainLayout = await AstroFile("main-layout", {
57
+ * path: "./src/layouts/MainLayout.astro",
58
+ * prompt: await alchemy`
59
+ * Create the main layout for an Astro site that:
60
+ * - Includes common head metadata and SEO optimization
61
+ * - Has slots for page content, header, and footer
62
+ * - Imports and uses the Header and Footer components
63
+ * - Sets up viewport and responsive configurations
64
+ * `,
65
+ * 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.",
66
+ * model: {
67
+ * id: "claude-3-opus-20240229",
68
+ * provider: "anthropic"
69
+ * }
70
+ * });
71
+ */
72
+ export const AstroFile = Resource("ai::AstroFile", async function (id, props) {
73
+ // Handle deletion phase
74
+ if (this.phase === "delete") {
75
+ return this.destroy();
76
+ }
77
+ // Use provided system prompt or default
78
+ const system = props.system || DEFAULT_ASTRO_SYSTEM_PROMPT;
79
+ // Generate initial content
80
+ const { text } = await generateText({
81
+ model: createModel(props),
82
+ prompt: props.prompt,
83
+ system,
84
+ providerOptions: props.model?.options,
85
+ ...(props.temperature === undefined
86
+ ? {}
87
+ : { temperature: props.temperature }),
88
+ });
89
+ // Extract and validate Astro code
90
+ let { code, error } = await extractAstroCode(text);
91
+ // Re-prompt if there are validation errors
92
+ if (error) {
93
+ const errorSystem = `${system}\n\nERROR: ${error}\n\nPlease try again and ensure your response contains exactly one Astro code block inside \`\`\`astro fences.`;
94
+ const { text: retryText } = await generateText({
95
+ model: createModel(props),
96
+ prompt: props.prompt,
97
+ system: errorSystem,
98
+ providerOptions: props.model?.options,
99
+ ...(props.temperature === undefined
100
+ ? {}
101
+ : { temperature: props.temperature }),
102
+ });
103
+ const retryResult = await extractAstroCode(retryText);
104
+ if (retryResult.error) {
105
+ throw new Error(`Failed to generate valid Astro code: ${retryResult.error}`);
106
+ }
107
+ code = retryResult.code;
108
+ }
109
+ // Format the code with Prettier
110
+ try {
111
+ // Set default parser to astro
112
+ const prettierOptions = {
113
+ parser: "astro",
114
+ ...props.prettierConfig,
115
+ };
116
+ // Format the code
117
+ code = await prettier.format(code, prettierOptions);
118
+ }
119
+ catch (error) {
120
+ // If Prettier formatting fails, just use the unformatted code
121
+ console.warn("Failed to format Astro code with Prettier:", error);
122
+ }
123
+ // Use StaticAstroFile to create/update the file
124
+ const file = await StaticAstroFile("file", props.path, code);
125
+ // Return the resource
126
+ return this({
127
+ ...props,
128
+ content: file.content,
129
+ createdAt: Date.now(),
130
+ updatedAt: Date.now(),
131
+ });
132
+ });
133
+ /**
134
+ * Extracts Astro code from between ```astro fences
135
+ * Validates that exactly one Astro code block exists
136
+ *
137
+ * @param text The text to extract Astro code from
138
+ * @returns The extracted Astro code or error message
139
+ */
140
+ async function extractAstroCode(text) {
141
+ const astroCodeRegex = /```astro\s*([\s\S]*?)```/g;
142
+ const matches = Array.from(text.matchAll(astroCodeRegex));
143
+ if (matches.length === 0) {
144
+ return {
145
+ code: "",
146
+ error: "No Astro code block found in the response. Please include your code within ```astro fences.",
147
+ };
148
+ }
149
+ if (matches.length > 1) {
150
+ return {
151
+ code: "",
152
+ error: "Multiple Astro code blocks found in the response. Please provide exactly one code block within ```astro fences.",
153
+ };
154
+ }
155
+ return { code: matches[0][1].trim() };
156
+ }
@@ -16,7 +16,7 @@ export interface ModelConfig {
16
16
  /**
17
17
  * Model-specific options
18
18
  */
19
- options?: Record<string, unknown>;
19
+ options?: Record<string, any>;
20
20
  }
21
21
  /**
22
22
  * Configuration for creating an OpenAI client
@@ -40,12 +40,11 @@ export interface ClientConfig {
40
40
  /**
41
41
  * Creates an OpenAI-compatible client with the given configuration
42
42
  */
43
- export declare function createClient(config: ClientConfig): import("@ai-sdk/openai-compatible").OpenAICompatibleProvider<string, string, string, string>;
43
+ export declare function createModel(config: ClientConfig): import("ai").LanguageModelV1;
44
44
  /**
45
- * Gets the model ID from the configuration or returns the default
45
+ * Handles rate limiting with exponential backoff
46
+ * @param fn Function to retry
47
+ * @returns Result of the function
48
+ * @throws Error if max retries or time is exceeded
46
49
  */
47
- export declare function getModelId(config: ClientConfig): string;
48
- /**
49
- * Gets the model options from the configuration
50
- */
51
- export declare function getModelOptions(config: ClientConfig): Record<string, unknown>;
50
+ export declare function withRateLimitRetry<T>(fn: () => Promise<T>): Promise<T>;
package/lib/ai/client.js CHANGED
@@ -1,29 +1,58 @@
1
- import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
1
+ import { anthropic } from "@ai-sdk/anthropic";
2
+ import { openai } from "@ai-sdk/openai";
2
3
  /**
3
4
  * Creates an OpenAI-compatible client with the given configuration
4
5
  */
5
- export function createClient(config) {
6
- // Get API key from props or environment
7
- const apiKey = config.apiKey?.unencrypted || process.env.OPENAI_API_KEY;
8
- if (!apiKey) {
9
- throw new Error("OpenAI API key is required");
6
+ export function createModel(config) {
7
+ if (config.model?.provider === "anthropic") {
8
+ return anthropic(config.model?.id ?? "claude-3-7-sonnet-latest");
9
+ }
10
+ else {
11
+ return openai(config.model?.id ?? "gpt-4o");
10
12
  }
11
- // Initialize OpenAI compatible provider
12
- return createOpenAICompatible({
13
- name: config.model?.provider || "openai",
14
- apiKey,
15
- baseURL: config.baseURL || "https://api.openai.com/v1",
16
- });
17
13
  }
18
14
  /**
19
- * Gets the model ID from the configuration or returns the default
15
+ * Maximum time to retry in milliseconds (5 minutes)
20
16
  */
21
- export function getModelId(config) {
22
- return config.model?.id || "gpt-4o";
23
- }
17
+ const MAX_RETRY_TIME = 5 * 60 * 1000;
18
+ /**
19
+ * Initial delay between retries in milliseconds
20
+ */
21
+ const INITIAL_RETRY_DELAY = 1000;
24
22
  /**
25
- * Gets the model options from the configuration
23
+ * Maximum number of retries
26
24
  */
27
- export function getModelOptions(config) {
28
- return config.model?.options || {};
25
+ const MAX_RETRIES = 10;
26
+ /**
27
+ * Handles rate limiting with exponential backoff
28
+ * @param fn Function to retry
29
+ * @returns Result of the function
30
+ * @throws Error if max retries or time is exceeded
31
+ */
32
+ export async function withRateLimitRetry(fn) {
33
+ let retryCount = 0;
34
+ let lastError = null;
35
+ let startTime = Date.now();
36
+ while (true) {
37
+ try {
38
+ return await fn();
39
+ }
40
+ catch (error) {
41
+ lastError = error;
42
+ console.log("retry error", error);
43
+ // Check if we should retry
44
+ const isRateLimit = error.statusCode === 429;
45
+ const timeElapsed = Date.now() - startTime;
46
+ const shouldRetry = isRateLimit && retryCount < MAX_RETRIES && timeElapsed < MAX_RETRY_TIME;
47
+ if (!shouldRetry) {
48
+ throw error;
49
+ }
50
+ // Calculate delay with exponential backoff
51
+ const delay = Math.min(INITIAL_RETRY_DELAY * Math.pow(2, retryCount), MAX_RETRY_TIME - timeElapsed);
52
+ console.log(`Retrying in ${delay}ms`);
53
+ // Wait before retrying
54
+ await new Promise((resolve) => setTimeout(resolve, delay));
55
+ retryCount++;
56
+ }
57
+ }
29
58
  }