alchemy 0.3.0 → 0.4.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 (154) hide show
  1. package/README.md +8 -3
  2. package/lib/ai/astro-file.js +7 -24
  3. package/lib/ai/client.d.ts +7 -0
  4. package/lib/ai/client.js +45 -0
  5. package/lib/ai/css-file.js +7 -24
  6. package/lib/ai/data.d.ts +26 -21
  7. package/lib/ai/data.js +33 -22
  8. package/lib/ai/document.d.ts +76 -5
  9. package/lib/ai/document.js +112 -56
  10. package/lib/ai/html-file.js +7 -24
  11. package/lib/ai/json-file.js +11 -32
  12. package/lib/ai/typescript-file.d.ts +1 -2
  13. package/lib/ai/typescript-file.js +8 -39
  14. package/lib/ai/vue-file.js +7 -24
  15. package/lib/ai/yaml-file.js +12 -48
  16. package/lib/alchemy.d.ts +5 -0
  17. package/lib/alchemy.js +61 -3
  18. package/lib/apply.js +18 -11
  19. package/lib/aws/account-id.d.ts +4 -1
  20. package/lib/aws/account-id.js +1 -1
  21. package/lib/aws/credentials.d.ts +5 -0
  22. package/lib/aws/credentials.js +0 -0
  23. package/lib/aws/oidc/oidc-provider.js +2 -2
  24. package/lib/aws/role.js +35 -23
  25. package/lib/cloudflare/account-api-token.d.ts +168 -0
  26. package/lib/cloudflare/account-api-token.js +179 -0
  27. package/lib/cloudflare/account-id.d.ts +1 -0
  28. package/lib/cloudflare/account-id.js +0 -0
  29. package/lib/cloudflare/api.d.ts +38 -1
  30. package/lib/cloudflare/api.js +55 -4
  31. package/lib/cloudflare/bucket.d.ts +118 -3
  32. package/lib/cloudflare/bucket.js +269 -89
  33. package/lib/cloudflare/custom-domain.d.ts +75 -0
  34. package/lib/cloudflare/custom-domain.js +154 -0
  35. package/lib/cloudflare/{dns.js → dns-records.js} +14 -1
  36. package/lib/cloudflare/generate-asset-manifest.js +1 -1
  37. package/lib/cloudflare/index.d.ts +5 -1
  38. package/lib/cloudflare/index.js +5 -1
  39. package/lib/cloudflare/permission-groups.d.ts +78 -0
  40. package/lib/cloudflare/permission-groups.js +48 -0
  41. package/lib/cloudflare/r2-rest-state-store.d.ts +2 -1
  42. package/lib/cloudflare/r2-rest-state-store.js +3 -2
  43. package/lib/cloudflare/static-site-router.js +16 -7
  44. package/lib/cloudflare/static-site.d.ts +17 -18
  45. package/lib/cloudflare/static-site.js +19 -7
  46. package/lib/cloudflare/upload-asset-manifest.js +4 -2
  47. package/lib/{util/encrypt.d.ts → encrypt.d.ts} +1 -1
  48. package/lib/{util/encrypt.js → encrypt.js} +1 -1
  49. package/lib/fs/copy-file.d.ts +54 -0
  50. package/lib/fs/copy-file.js +63 -0
  51. package/lib/fs/file-system-state-store.js +1 -1
  52. package/lib/fs/file.d.ts +31 -0
  53. package/lib/fs/file.js +30 -5
  54. package/lib/fs/index.d.ts +6 -0
  55. package/lib/fs/index.js +6 -0
  56. package/lib/fs/static-astro-file.d.ts +34 -0
  57. package/lib/fs/static-astro-file.js +39 -0
  58. package/lib/fs/static-css-file.d.ts +24 -0
  59. package/lib/fs/static-css-file.js +29 -0
  60. package/lib/fs/static-html-file.d.ts +31 -0
  61. package/lib/fs/static-html-file.js +36 -0
  62. package/lib/fs/static-json-file.d.ts +2 -2
  63. package/lib/fs/static-json-file.js +3 -2
  64. package/lib/fs/static-text-file.d.ts +1 -1
  65. package/lib/fs/static-text-file.js +3 -2
  66. package/lib/fs/static-typescript-file.d.ts +2 -2
  67. package/lib/fs/static-typescript-file.js +3 -2
  68. package/lib/fs/static-vue-file.d.ts +28 -0
  69. package/lib/fs/static-vue-file.js +33 -0
  70. package/lib/fs/static-yaml-file.d.ts +11 -8
  71. package/lib/fs/static-yaml-file.js +3 -2
  72. package/lib/internal/{providers.d.ts → docs/providers.d.ts} +14 -4
  73. package/lib/internal/docs/providers.js +196 -0
  74. package/lib/secret.d.ts +3 -0
  75. package/lib/secret.js +13 -0
  76. package/lib/{util/serde.d.ts → serde.d.ts} +1 -1
  77. package/lib/{util/serde.js → serde.js} +12 -4
  78. package/lib/test/bun.js +3 -2
  79. package/lib/util/sha256.d.ts +1 -0
  80. package/lib/util/sha256.js +4 -0
  81. package/lib/web/vitepress/config.d.ts +11 -0
  82. package/lib/web/vitepress/config.js +32 -0
  83. package/lib/web/vitepress/home-page.d.ts +5 -3
  84. package/lib/web/vitepress/home-page.js +48 -35
  85. package/lib/web/vitepress/index.d.ts +2 -0
  86. package/lib/web/vitepress/index.js +2 -0
  87. package/lib/web/vitepress/process-front-matter-files.d.ts +18 -0
  88. package/lib/web/vitepress/process-front-matter-files.js +68 -0
  89. package/lib/web/vitepress/vitepress.d.ts +5 -4
  90. package/lib/web/vitepress/vitepress.js +20 -36
  91. package/package.json +8 -4
  92. package/src/ai/astro-file.ts +11 -30
  93. package/src/ai/client.ts +59 -0
  94. package/src/ai/css-file.ts +11 -30
  95. package/src/ai/data.ts +50 -25
  96. package/src/ai/document.ts +171 -66
  97. package/src/ai/html-file.ts +11 -30
  98. package/src/ai/json-file.ts +13 -37
  99. package/src/ai/typescript-file.ts +14 -48
  100. package/src/ai/vue-file.ts +11 -30
  101. package/src/ai/yaml-file.ts +13 -53
  102. package/src/alchemy.ts +74 -9
  103. package/src/apply.ts +27 -26
  104. package/src/aws/account-id.ts +6 -2
  105. package/src/aws/credentials.ts +6 -0
  106. package/src/aws/oidc/oidc-provider.ts +17 -17
  107. package/src/aws/role.ts +68 -54
  108. package/src/cloudflare/account-api-token.ts +365 -0
  109. package/src/cloudflare/account-id.ts +0 -0
  110. package/src/cloudflare/api.ts +88 -17
  111. package/src/cloudflare/bucket.ts +493 -133
  112. package/src/cloudflare/custom-domain.ts +318 -0
  113. package/src/cloudflare/{dns.ts → dns-records.ts} +36 -22
  114. package/src/cloudflare/generate-asset-manifest.ts +4 -4
  115. package/src/cloudflare/index.ts +5 -1
  116. package/src/cloudflare/permission-groups.ts +137 -0
  117. package/src/cloudflare/r2-rest-state-store.ts +14 -13
  118. package/src/cloudflare/static-site-router.ts +18 -8
  119. package/src/cloudflare/static-site.ts +27 -35
  120. package/src/cloudflare/upload-asset-manifest.ts +6 -4
  121. package/src/cloudflare/worker.ts +44 -43
  122. package/src/dns/import-dns.ts +4 -4
  123. package/src/{util/encrypt.ts → encrypt.ts} +7 -10
  124. package/src/fs/copy-file.ts +115 -0
  125. package/src/fs/file-system-state-store.ts +5 -5
  126. package/src/fs/file.ts +64 -9
  127. package/src/fs/index.ts +6 -0
  128. package/src/fs/static-astro-file.ts +45 -0
  129. package/src/fs/static-css-file.ts +35 -0
  130. package/src/fs/static-html-file.ts +42 -0
  131. package/src/fs/static-json-file.ts +4 -3
  132. package/src/fs/static-text-file.ts +5 -2
  133. package/src/fs/static-typescript-file.ts +4 -3
  134. package/src/fs/static-vue-file.ts +39 -0
  135. package/src/fs/static-yaml-file.ts +13 -9
  136. package/src/internal/docs/providers.ts +281 -0
  137. package/src/secret.ts +16 -0
  138. package/src/{util/serde.ts → serde.ts} +16 -10
  139. package/src/test/bun.ts +8 -7
  140. package/src/util/sha256.ts +5 -0
  141. package/src/web/vitepress/config.ts +50 -0
  142. package/src/web/vitepress/home-page.ts +54 -42
  143. package/src/web/vitepress/index.ts +2 -0
  144. package/src/web/vitepress/process-front-matter-files.ts +98 -0
  145. package/src/web/vitepress/vitepress.ts +34 -50
  146. package/lib/internal/getting-started.d.ts +0 -10
  147. package/lib/internal/getting-started.js +0 -94
  148. package/lib/internal/project.d.ts +0 -13
  149. package/lib/internal/project.js +0 -101
  150. package/lib/internal/providers.js +0 -148
  151. package/src/internal/getting-started.ts +0 -107
  152. package/src/internal/project.ts +0 -119
  153. package/src/internal/providers.ts +0 -209
  154. /package/lib/cloudflare/{dns.d.ts → dns-records.d.ts} +0 -0
@@ -1,10 +1,8 @@
1
1
  import { generateText } from "ai";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
4
2
  import type { Context } from "../context";
3
+ import { StaticCSSFile } from "../fs/static-css-file";
5
4
  import { Resource } from "../resource";
6
5
  import type { Secret } from "../secret";
7
- import { ignore } from "../util/ignore";
8
6
  import { type ModelConfig, createModel } from "./client";
9
7
 
10
8
  /**
@@ -154,20 +152,10 @@ export const CSSFile = Resource(
154
152
  async function (
155
153
  this: Context<CSSFile>,
156
154
  id: string,
157
- props: CSSFileProps,
155
+ props: CSSFileProps
158
156
  ): Promise<CSSFile> {
159
- // Ensure directory exists
160
- await fs.mkdir(path.dirname(props.path), { recursive: true });
161
-
157
+ // Handle deletion phase
162
158
  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
159
  return this.destroy();
172
160
  }
173
161
 
@@ -206,31 +194,24 @@ export const CSSFile = Resource(
206
194
 
207
195
  if (retryResult.error) {
208
196
  throw new Error(
209
- `Failed to generate valid CSS code: ${retryResult.error}\n${retryText}`,
197
+ `Failed to generate valid CSS code: ${retryResult.error}\n${retryText}`
210
198
  );
211
199
  }
212
200
 
213
201
  code = retryResult.code;
214
202
  }
215
203
 
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);
204
+ // Use StaticCSSFile to create/update the file
205
+ const file = await StaticCSSFile("file", props.path, code);
225
206
 
226
207
  // Return the resource
227
208
  return this({
228
209
  ...props,
229
- content: code,
230
- createdAt: stats.birthtimeMs,
231
- updatedAt: stats.mtimeMs,
210
+ content: file.content,
211
+ createdAt: Date.now(),
212
+ updatedAt: Date.now(),
232
213
  });
233
- },
214
+ }
234
215
  );
235
216
 
236
217
  /**
@@ -241,7 +222,7 @@ export const CSSFile = Resource(
241
222
  * @returns The extracted CSS code or error message
242
223
  */
243
224
  async function extractCSSCode(
244
- text: string,
225
+ text: string
245
226
  ): Promise<{ code: string; error?: string }> {
246
227
  const cssCodeRegex = /```css\s*([\s\S]*?)```/g;
247
228
  const matches = Array.from(text.matchAll(cssCodeRegex));
package/src/ai/data.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { generateObject } from "ai";
1
+ import { generateObject, type CoreMessage } from "ai";
2
2
  import type { JsonSchema, Type, type } from "arktype";
3
3
  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 { type ModelConfig, createModel } from "./client";
7
+ import { createModel, type ModelConfig } from "./client";
8
8
 
9
9
  /**
10
10
  * Properties for creating or updating an AI Object
@@ -24,7 +24,13 @@ export interface DataProps<T extends Type<any, any>> {
24
24
  * ${alchemy.file("src/data.ts")}
25
25
  * `
26
26
  */
27
- prompt: string;
27
+ prompt?: string;
28
+
29
+ /**
30
+ * Message history for the conversation
31
+ * If provided, this will be used instead of the prompt
32
+ */
33
+ messages?: CoreMessage[];
28
34
 
29
35
  /**
30
36
  * System prompt to guide the AI's behavior
@@ -70,6 +76,11 @@ export interface Data<T> extends Resource<"ai::Object"> {
70
76
  */
71
77
  object: T;
72
78
 
79
+ /**
80
+ * Updated message history with the AI's response appended
81
+ */
82
+ messages: CoreMessage[];
83
+
73
84
  /**
74
85
  * Time at which the content was generated
75
86
  */
@@ -127,28 +138,23 @@ export interface Data<T> extends Resource<"ai::Object"> {
127
138
  * });
128
139
  *
129
140
  * @example
130
- * // Using specific model configuration with advanced options
131
- * const analysisSchema = type({
132
- * insights: "string[]",
133
- * recommendations: "string[]",
134
- * risk: "'low'|'medium'|'high'"
141
+ * // Using message history for iterative generation
142
+ * const feedbackSchema = type({
143
+ * rating: "number",
144
+ * positives: "string[]",
145
+ * improvements: "string[]",
146
+ * summary: "string"
135
147
  * });
136
148
  *
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
149
+ * const feedback = await Data("product-feedback", {
150
+ * schema: feedbackSchema,
151
+ * messages: [
152
+ * { role: "user", content: "I'd like feedback on my product design" },
153
+ * { role: "assistant", content: "I'd be happy to provide feedback. What's your product?" },
154
+ * { role: "user", content: "It's a new smart home device that..." }
155
+ * ],
156
+ * system: "You are a product design expert providing structured feedback",
157
+ * temperature: 0.3
152
158
  * });
153
159
  */
154
160
  export const Data = Resource("ai::Object", async function <
@@ -160,6 +166,14 @@ export const Data = Resource("ai::Object", async function <
160
166
  return this.destroy();
161
167
  }
162
168
 
169
+ // Validate that either prompt or messages is provided
170
+ if (!props.prompt && !props.messages) {
171
+ throw new Error("Either prompt or messages must be provided");
172
+ }
173
+
174
+ // Create messages array if only prompt is provided
175
+ const messages = props.messages || [{ role: "user", content: props.prompt! }];
176
+
163
177
  // Generate structured output using generateObject
164
178
  const { object } = await generateObject({
165
179
  model: createModel(props),
@@ -170,17 +184,28 @@ export const Data = Resource("ai::Object", async function <
170
184
  system:
171
185
  props.system ||
172
186
  "You are an AI assistant tasked with generating structured content.",
173
- prompt: props.prompt,
187
+ messages,
174
188
  ...(props.temperature === undefined
175
189
  ? {}
176
190
  : // some models error if you provide it (rather than ignoring it)
177
191
  { temperature: props.temperature }),
178
192
  });
179
193
 
180
- // Return the resource with typed content
194
+ // Create updated message history with the structured response
195
+ const responseText = JSON.stringify(object);
196
+ const updatedMessages = [
197
+ ...messages,
198
+ {
199
+ role: "assistant" as const,
200
+ content: responseText,
201
+ },
202
+ ];
203
+
204
+ // Return the resource with typed content and updated messages
181
205
  return this({
182
206
  type: props.schema,
183
207
  object: object,
208
+ messages: updatedMessages,
184
209
  createdAt: Date.now(),
185
210
  });
186
211
  });
@@ -1,11 +1,9 @@
1
- import { generateText } from "ai";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
1
+ import { generateText, type CoreMessage } from "ai";
4
2
  import type { Context } from "../context";
3
+ import { StaticTextFile } from "../fs/static-text-file";
5
4
  import { Resource } from "../resource";
6
5
  import type { Secret } from "../secret";
7
- import { ignore } from "../util/ignore";
8
- import { type ModelConfig, createModel } from "./client";
6
+ import { createModel, withRateLimitRetry, type ModelConfig } from "./client";
9
7
 
10
8
  /**
11
9
  * Properties for creating or updating a Document
@@ -13,13 +11,16 @@ import { type ModelConfig, createModel } from "./client";
13
11
  export interface DocumentProps {
14
12
  /**
15
13
  * Title of the document
14
+ *
15
+ * @default id
16
16
  */
17
- title: string;
17
+ title?: string;
18
18
 
19
19
  /**
20
- * Path to the markdown document
20
+ * Optional path to the markdown document
21
+ * If provided, document will be written to this path
21
22
  */
22
- path: string;
23
+ path?: string;
23
24
 
24
25
  /**
25
26
  * Base URL for the OpenAI API
@@ -36,7 +37,19 @@ export interface DocumentProps {
36
37
  * ${alchemy.file("src/api.ts")}
37
38
  * `
38
39
  */
39
- prompt: string;
40
+ prompt?: string;
41
+
42
+ /**
43
+ * Message history for conversation-based generation
44
+ * If provided, this will be used instead of the prompt
45
+ * @example
46
+ * messages: [
47
+ * { role: "user", content: "Generate API documentation for this file" },
48
+ * { role: "assistant", content: "I'll create detailed API docs. What file should I document?" },
49
+ * { role: "user", content: "Please document src/api.ts" }
50
+ * ]
51
+ */
52
+ messages?: CoreMessage[];
40
53
 
41
54
  /**
42
55
  * System prompt for the model
@@ -64,17 +77,40 @@ export interface DocumentProps {
64
77
  * @default 0.7
65
78
  */
66
79
  temperature?: number;
80
+
81
+ /**
82
+ * Maximum number of tokens to generate.
83
+ * Higher values allow for longer documents but may increase cost and generation time.
84
+ * @default 10000
85
+ */
86
+ maxTokens?: number;
87
+
88
+ /**
89
+ * Freeze the document after creation (do not re-generate on updates)
90
+ * @default false
91
+ */
92
+ freeze?: boolean;
67
93
  }
68
94
 
69
95
  /**
70
96
  * A markdown document that can be created, updated, and deleted
71
97
  */
72
98
  export interface Document extends DocumentProps, Resource<"docs::Document"> {
99
+ /**
100
+ * The title of the document
101
+ */
102
+ title: string;
103
+
73
104
  /**
74
105
  * Content of the document
75
106
  */
76
107
  content: string;
77
108
 
109
+ /**
110
+ * Updated message history with the document response appended
111
+ */
112
+ messages: CoreMessage[];
113
+
78
114
  /**
79
115
  * Time at which the document was created
80
116
  */
@@ -84,6 +120,11 @@ export interface Document extends DocumentProps, Resource<"docs::Document"> {
84
120
  * Time at which the document was last updated
85
121
  */
86
122
  updatedAt: number;
123
+
124
+ /**
125
+ * File resource if path was provided
126
+ */
127
+ file?: StaticTextFile;
87
128
  }
88
129
 
89
130
  /**
@@ -97,7 +138,22 @@ const DEFAULT_MD_SYSTEM_PROMPT =
97
138
  * Supports powerful context handling through the alchemy template literal tag.
98
139
  *
99
140
  * @example
100
- * // Create a document using alchemy template literals for context
141
+ * // Create an in-memory document (no file created)
142
+ * const apiDocs = await Document("api-docs", {
143
+ * title: "API Documentation",
144
+ * prompt: await alchemy`
145
+ * Generate API documentation based on these source files:
146
+ * ${alchemy.file("src/api.ts")}
147
+ * ${alchemy.file("src/types.ts")}
148
+ * `,
149
+ * model: {
150
+ * id: "gpt-4o",
151
+ * provider: "openai"
152
+ * }
153
+ * });
154
+ *
155
+ * @example
156
+ * // Create a document and write it to disk
101
157
  * const apiDocs = await Document("api-docs", {
102
158
  * title: "API Documentation",
103
159
  * path: "./docs/api.md",
@@ -113,6 +169,23 @@ const DEFAULT_MD_SYSTEM_PROMPT =
113
169
  * });
114
170
  *
115
171
  * @example
172
+ * // Use message history for iterative document generation
173
+ * const apiDocs = await Document("api-docs", {
174
+ * title: "API Documentation",
175
+ * path: "./docs/api.md",
176
+ * messages: [
177
+ * { role: "user", content: "Create API documentation for these files" },
178
+ * { role: "assistant", content: "I'll help you create API documentation. Please provide the files." },
179
+ * { role: "user", content: "Here are the files: [file contents]" }
180
+ * ],
181
+ * system: "You are a technical documentation writer. Generate clear and concise API documentation.",
182
+ * model: {
183
+ * id: "gpt-4o",
184
+ * provider: "openai"
185
+ * }
186
+ * });
187
+ *
188
+ * @example
116
189
  * // Use alchemy template literals with file collections and temperature control
117
190
  * const modelDocs = await Document("models", {
118
191
  * title: "Data Models",
@@ -149,84 +222,104 @@ export const Document = Resource(
149
222
  async function (
150
223
  this: Context<Document>,
151
224
  id: string,
152
- props: DocumentProps,
225
+ props: DocumentProps
153
226
  ): Promise<Document> {
154
- // Ensure directory exists
155
- await fs.mkdir(path.dirname(props.path), { recursive: true });
227
+ // Validate that either prompt or messages are provided
228
+ if (!props.prompt && !props.messages) {
229
+ throw new Error("Either prompt or messages must be provided");
230
+ }
156
231
 
232
+ // Handle deletion phase
157
233
  if (this.phase === "delete") {
158
- try {
159
- await fs.unlink(props.path);
160
- } catch (error: any) {
161
- // Ignore if file doesn't exist
162
- if (error.code !== "ENOENT") {
163
- throw error;
164
- }
165
- }
166
234
  return this.destroy();
167
235
  }
168
236
 
237
+ if (this.phase === "update" && props.freeze) {
238
+ if (props.path) {
239
+ const filePath = props.path;
240
+ const fileId = `${id}-file`;
241
+
242
+ await StaticTextFile(fileId, filePath, this.output!.content);
243
+ }
244
+ return this(this.output);
245
+ }
246
+
169
247
  // Use provided system prompt or default
170
248
  const system = props.system || DEFAULT_MD_SYSTEM_PROMPT;
171
249
 
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
- : // some models error if you provide it (rather than ignoring it)
181
- { temperature: props.temperature }),
250
+ // Generate initial content with rate limit retry
251
+ const { text } = await withRateLimitRetry(async () => {
252
+ return generateText({
253
+ model: createModel(props),
254
+ ...(props.messages
255
+ ? { messages: props.messages }
256
+ : { prompt: props.prompt! }),
257
+ system,
258
+ maxTokens: props.maxTokens || 8192,
259
+ providerOptions: props.model?.options,
260
+ ...(props.temperature === undefined
261
+ ? {}
262
+ : // some models error if you provide it (rather than ignoring it)
263
+ { temperature: props.temperature }),
264
+ });
182
265
  });
183
266
 
184
267
  // Extract and validate markdown content
185
- let { content, error } = await extractMarkdownContent(text);
268
+ let { content, error } = extractMarkdownContent(text);
186
269
 
187
270
  // Re-prompt if there are validation errors
188
271
  if (error) {
189
272
  const errorSystem = `${system}\n\nERROR: ${error}\n\nPlease try again and ensure your response contains exactly one markdown document inside \`\`\`md fences.`;
190
273
 
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 }),
274
+ const { text: retryText } = await withRateLimitRetry(async () => {
275
+ return generateText({
276
+ model: createModel(props),
277
+ ...(props.messages
278
+ ? { messages: props.messages }
279
+ : { prompt: props.prompt! }),
280
+ system: errorSystem,
281
+ providerOptions: props.model?.options,
282
+ ...(props.temperature === undefined
283
+ ? {}
284
+ : { temperature: props.temperature }),
285
+ });
199
286
  });
200
287
 
201
- const retryResult = await extractMarkdownContent(retryText);
288
+ const retryResult = extractMarkdownContent(retryText);
202
289
 
203
290
  if (retryResult.error) {
204
291
  throw new Error(
205
- `Failed to generate valid markdown content: ${retryResult.error}`,
292
+ `Failed to generate valid markdown content: ${retryResult.error}\n${retryText}`
206
293
  );
207
294
  }
208
295
 
209
296
  content = retryResult.content;
210
297
  }
211
298
 
212
- if (this.phase === "update" && props.path !== this.props.path) {
213
- await ignore("ENOENT", () => fs.unlink(this.props.path));
214
- }
299
+ // Create result object
300
+ const result: Partial<Document> = {
301
+ ...props,
302
+ content,
303
+ messages: [
304
+ ...(props.messages || [{ role: "user", content: props.prompt! }]),
305
+ { role: "assistant", content },
306
+ ],
307
+ createdAt: this.output?.createdAt || Date.now(),
308
+ updatedAt: Date.now(),
309
+ title: props.title || id,
310
+ };
215
311
 
216
- // Write content to file
217
- await fs.writeFile(props.path, content);
312
+ // Write file if path is provided
313
+ if (props.path) {
314
+ const filePath = props.path;
315
+ const fileId = `${id}-file`;
218
316
 
219
- // Get file stats for timestamps
220
- const stats = await fs.stat(props.path);
317
+ result.file = await StaticTextFile(fileId, filePath, content);
318
+ }
221
319
 
222
320
  // Return the resource
223
- return this({
224
- ...props,
225
- content: content,
226
- createdAt: stats.birthtimeMs,
227
- updatedAt: stats.mtimeMs,
228
- });
229
- },
321
+ return this(result as Document);
322
+ }
230
323
  );
231
324
 
232
325
  /**
@@ -236,13 +329,14 @@ export const Document = Resource(
236
329
  * @param text The text to extract markdown content from
237
330
  * @returns The extracted markdown content or error message
238
331
  */
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));
332
+ function extractMarkdownContent(text: string): {
333
+ content: string;
334
+ error?: string;
335
+ } {
336
+ const lines = text.split("\n");
337
+ const startIdx = lines.findIndex((line) => line.trim() === "```md");
244
338
 
245
- if (matches.length === 0) {
339
+ if (startIdx === -1) {
246
340
  return {
247
341
  content: "",
248
342
  error:
@@ -250,13 +344,24 @@ async function extractMarkdownContent(
250
344
  };
251
345
  }
252
346
 
253
- if (matches.length > 1) {
347
+ const rest = lines.slice(startIdx + 1);
348
+ const endRelativeIdx = rest
349
+ .map((line) => line.trim() === "```")
350
+ .lastIndexOf(true);
351
+
352
+ if (endRelativeIdx === -1) {
254
353
  return {
255
354
  content: "",
256
- error:
257
- "Multiple markdown code blocks found in the response. Please provide exactly one markdown block within ```md fences.",
355
+ error: "Markdown block was not closed properly.",
258
356
  };
259
357
  }
260
358
 
261
- return { content: matches[0][1].trim() };
359
+ const endIdx = startIdx + 1 + endRelativeIdx;
360
+
361
+ const content = lines
362
+ .slice(startIdx + 1, endIdx)
363
+ .join("\n")
364
+ .trim();
365
+
366
+ return { content };
262
367
  }
@@ -1,10 +1,8 @@
1
1
  import { generateText } from "ai";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
4
2
  import type { Context } from "../context";
3
+ import { StaticHTMLFile } from "../fs/static-html-file";
5
4
  import { Resource } from "../resource";
6
5
  import type { Secret } from "../secret";
7
- import { ignore } from "../util/ignore";
8
6
  import { type ModelConfig, createModel } from "./client";
9
7
 
10
8
  /**
@@ -151,20 +149,10 @@ export const HTMLFile = Resource(
151
149
  async function (
152
150
  this: Context<HTMLFile>,
153
151
  id: string,
154
- props: HTMLFileProps,
152
+ props: HTMLFileProps
155
153
  ): Promise<HTMLFile> {
156
- // Ensure directory exists
157
- await fs.mkdir(path.dirname(props.path), { recursive: true });
158
-
154
+ // Handle deletion phase
159
155
  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
156
  return this.destroy();
169
157
  }
170
158
 
@@ -203,31 +191,24 @@ export const HTMLFile = Resource(
203
191
 
204
192
  if (retryResult.error) {
205
193
  throw new Error(
206
- `Failed to generate valid HTML code: ${retryResult.error}`,
194
+ `Failed to generate valid HTML code: ${retryResult.error}`
207
195
  );
208
196
  }
209
197
 
210
198
  code = retryResult.code;
211
199
  }
212
200
 
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);
201
+ // Use StaticHTMLFile to create/update the file
202
+ const file = await StaticHTMLFile("file", props.path, code);
222
203
 
223
204
  // Return the resource
224
205
  return this({
225
206
  ...props,
226
- content: code,
227
- createdAt: stats.birthtimeMs,
228
- updatedAt: stats.mtimeMs,
207
+ content: file.content,
208
+ createdAt: Date.now(),
209
+ updatedAt: Date.now(),
229
210
  });
230
- },
211
+ }
231
212
  );
232
213
 
233
214
  /**
@@ -238,7 +219,7 @@ export const HTMLFile = Resource(
238
219
  * @returns The extracted HTML code or error message
239
220
  */
240
221
  async function extractHTMLCode(
241
- text: string,
222
+ text: string
242
223
  ): Promise<{ code: string; error?: string }> {
243
224
  const htmlCodeRegex = /```html\s*([\s\S]*?)```/g;
244
225
  const matches = Array.from(text.matchAll(htmlCodeRegex));