alchemy 0.1.1 → 0.1.3

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 (54) hide show
  1. package/README.md +324 -0
  2. package/package.json +15 -8
  3. package/src/$.ts +36 -0
  4. package/src/agent/ai.ts +215 -0
  5. package/src/{components/agent → agent}/anthropic.ts +6 -2
  6. package/src/agent/dependencies.ts +62 -0
  7. package/src/agent/file-context.ts +8 -0
  8. package/src/agent/index.ts +9 -0
  9. package/src/agent/model.ts +39 -0
  10. package/src/agent/module.ts +418 -0
  11. package/src/{components/agent → agent}/openai.ts +6 -0
  12. package/src/agent/program.md +38 -0
  13. package/src/agent/program.ts +37 -0
  14. package/src/agent/prompts.ts +20 -0
  15. package/src/alchemize.ts +45 -14
  16. package/src/apply.ts +25 -10
  17. package/src/{components/aws → aws}/bucket.ts +2 -2
  18. package/src/{components/aws → aws}/function.ts +2 -2
  19. package/src/{components/aws → aws}/policy.ts +2 -2
  20. package/src/{components/aws → aws}/queue.ts +1 -1
  21. package/src/{components/aws → aws}/role.ts +2 -2
  22. package/src/{components/aws → aws}/table.ts +2 -2
  23. package/src/destroy.ts +36 -12
  24. package/src/{components/esbuild.ts → esbuild.ts} +1 -1
  25. package/src/{components/fs.ts → fs.ts} +20 -18
  26. package/src/global.ts +7 -14
  27. package/src/index.ts +11 -2
  28. package/src/input.ts +1 -1
  29. package/src/markdown/design.ts +125 -0
  30. package/src/markdown/extract.ts +9 -0
  31. package/src/markdown/index.ts +3 -0
  32. package/src/{components/agent → markdown}/requirements.ts +53 -29
  33. package/src/output.ts +6 -0
  34. package/src/resource.ts +208 -75
  35. package/src/scope.ts +56 -0
  36. package/src/slug.ts +3 -0
  37. package/src/state.ts +66 -38
  38. package/src/typescript/check-omission.ts +47 -0
  39. package/src/typescript/debug-type-errors.ts +42 -0
  40. package/src/typescript/extract.ts +9 -0
  41. package/src/{components/agent → typescript}/index.ts +3 -1
  42. package/src/typescript/install-packages.ts +50 -0
  43. package/src/{components/agent → typescript}/package.ts +44 -27
  44. package/src/typescript/repair-omissions.ts +31 -0
  45. package/src/typescript/repair.ts +45 -0
  46. package/src/{components/agent → typescript}/tsconfig.ts +37 -17
  47. package/src/typescript/typescript.ts +217 -0
  48. package/src/typescript/validate.ts +73 -0
  49. package/test/scope.test.ts +67 -0
  50. package/src/components/agent/agent.ts +0 -45
  51. package/src/components/agent/model.ts +0 -22
  52. package/src/components/agent/typescript.ts +0 -177
  53. /package/src/{components/agent → agent}/scrape.ts +0 -0
  54. /package/src/{components/aws → aws}/index.ts +0 -0
package/src/alchemize.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  import { evaluate } from "./apply";
2
2
  import { destroy } from "./destroy";
3
- import { defaultStage, nodes, providers, stateStore } from "./global";
4
- import type { StateStore } from "./state";
5
-
6
- let finalized = false;
3
+ import { defaultStage, defaultStateStore, providers } from "./global";
4
+ import { type Scope, getScope } from "./scope";
5
+ import type { StateStoreType } from "./state";
7
6
 
8
7
  export interface AlchemizeOptions {
9
8
  /**
@@ -27,7 +26,17 @@ export interface AlchemizeOptions {
27
26
  /**
28
27
  * A custom state store to use instead of the default file system store.
29
28
  */
30
- stateStore?: StateStore;
29
+ stateStore?: StateStoreType;
30
+ /**
31
+ * A custom scope to use instead of the default root scope.
32
+ */
33
+ scope?: Scope;
34
+ /**
35
+ * If true, will not print any Create/Update/Delete messages.
36
+ *
37
+ * @default false
38
+ */
39
+ quiet?: boolean;
31
40
  }
32
41
 
33
42
  /**
@@ -36,20 +45,21 @@ export interface AlchemizeOptions {
36
45
  * By default, this will be called when
37
46
  */
38
47
  export async function alchemize(options?: AlchemizeOptions) {
39
- if (finalized) {
40
- return;
41
- }
42
- finalized = true;
43
-
48
+ const scope = options?.scope ?? getScope();
49
+ const nodes = scope.nodes;
44
50
  const stage = options?.stage ?? defaultStage;
45
- const state = options?.stateStore ?? stateStore;
51
+ const stateStore = new (options?.stateStore ?? defaultStateStore)(
52
+ scope.getScopePath(stage),
53
+ );
46
54
 
47
- await state.init?.();
55
+ if (nodes.size > 0) {
56
+ await stateStore.init?.();
57
+ }
48
58
 
49
59
  // Track in-progress deletions to avoid duplicate work
50
60
  const deletionPromises = new Map<string, Promise<void>>();
51
61
 
52
- const priorStates = await state.all(stage);
62
+ const priorStates = await stateStore.all();
53
63
  const priorIDs = Object.keys(priorStates);
54
64
 
55
65
  const mode = options?.mode ?? "up";
@@ -61,6 +71,9 @@ export async function alchemize(options?: AlchemizeOptions) {
61
71
  .map((node) =>
62
72
  evaluate(node.resource, {
63
73
  stage,
74
+ scope,
75
+ stateStore,
76
+ quiet: options?.quiet,
64
77
  }),
65
78
  ),
66
79
  );
@@ -78,6 +91,10 @@ export async function alchemize(options?: AlchemizeOptions) {
78
91
 
79
92
  const orphanIDs = Array.from(priorIDs).filter((id) => !aliveIDs.has(id));
80
93
 
94
+ console.log("orphanIDs", orphanIDs);
95
+ console.log("priorIDs", priorIDs);
96
+ console.log("aliveIDs", aliveIDs);
97
+
81
98
  const orphanStates = Object.fromEntries(
82
99
  orphanIDs.map((id) => [id, priorStates[id]] as const),
83
100
  );
@@ -89,6 +106,10 @@ export async function alchemize(options?: AlchemizeOptions) {
89
106
  (orphanGraph[dep] ??= new Set()).add(orphanID);
90
107
  }
91
108
  }
109
+ if (Object.keys(orphanGraph).length > 0) {
110
+ console.log(stateStore.path);
111
+ console.log(orphanGraph);
112
+ }
92
113
 
93
114
  // Start deletion from each orphan that has no dependents (nothing depends on it)
94
115
  await Promise.all(
@@ -97,6 +118,11 @@ export async function alchemize(options?: AlchemizeOptions) {
97
118
  .map((id) => deleteOrphan(id)),
98
119
  );
99
120
 
121
+ const remaining = await stateStore.count();
122
+ if (remaining === 0) {
123
+ await stateStore.deinit?.();
124
+ }
125
+
100
126
  // Recursively delete orphans in dependency order
101
127
  async function deleteOrphan(orphanID: string): Promise<void> {
102
128
  // Return existing deletion promise if this orphan is already being deleted
@@ -125,7 +151,12 @@ export async function alchemize(options?: AlchemizeOptions) {
125
151
  `No provider found for ${providerType}. Did you forget to import it?`,
126
152
  );
127
153
  }
128
- await destroy(stage, orphanID, orphanState, provider);
154
+ if (!options?.quiet) {
155
+ console.log("Destroy:", orphanID);
156
+ }
157
+ await destroy(stage, scope, orphanID, orphanState, provider, stateStore, {
158
+ quiet: options?.quiet,
159
+ });
129
160
 
130
161
  // After this resource is deleted, we can delete its dependencies if they're orphans
131
162
  if (orphanState.deps.length > 0) {
package/src/apply.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { defaultStage, stateStore } from "./global";
1
+ import { defaultStage, defaultStateStore } from "./global";
2
2
  import { Output } from "./output";
3
3
  import {
4
4
  Input,
@@ -7,11 +7,14 @@ import {
7
7
  ResourceID,
8
8
  isResource,
9
9
  } from "./resource";
10
+ import { type Scope as IScope, getScope } from "./scope";
10
11
  import type { StateStore } from "./state";
11
12
 
12
- interface ApplyOptions {
13
- stage?: string;
14
- stateStore?: StateStore;
13
+ export interface ApplyOptions {
14
+ stage: string;
15
+ scope: IScope;
16
+ stateStore: StateStore;
17
+ quiet?: boolean;
15
18
  }
16
19
 
17
20
  /**
@@ -21,9 +24,16 @@ interface ApplyOptions {
21
24
  */
22
25
  export async function apply<T>(
23
26
  output: T | Output<T>,
24
- options?: ApplyOptions,
27
+ options?: Partial<ApplyOptions>,
25
28
  ): Promise<T> {
26
- return (await evaluate(output, options)).value;
29
+ const stage = options?.stage ?? defaultStage;
30
+ const scope = options?.scope ?? getScope();
31
+ const statePath = scope.getScopePath(stage);
32
+ const stateStore = options?.stateStore ?? new defaultStateStore(statePath);
33
+
34
+ return (
35
+ await evaluate(output, { stage, scope, stateStore, quiet: options?.quiet })
36
+ ).value;
27
37
  }
28
38
 
29
39
  class Evaluated<T> {
@@ -37,9 +47,10 @@ const cache = new WeakMap<Resource, Promise<Evaluated<any>>>();
37
47
 
38
48
  export async function evaluate<T>(
39
49
  output: T | Output<T>,
40
- options: ApplyOptions = {},
50
+ options: ApplyOptions,
41
51
  ): Promise<Evaluated<T>> {
42
- const state = options.stateStore ?? stateStore;
52
+ const stage = options.stage;
53
+ const stateStore = options.stateStore;
43
54
  if (isResource(output)) {
44
55
  const resource = output;
45
56
  const resourceID = resource[ResourceID];
@@ -63,14 +74,16 @@ export async function evaluate<T>(
63
74
  const inputs = evaluated.map((input) => input.value);
64
75
  try {
65
76
  const result: T = await resource[Provider].update(
66
- options.stage ?? defaultStage,
77
+ stage,
67
78
  resource,
68
79
  deps,
69
80
  inputs as [],
70
- state,
81
+ stateStore,
82
+ options,
71
83
  );
72
84
  resolve!(new Evaluated(result, [resourceID, ...deps]));
73
85
  } catch (error) {
86
+ console.error(error);
74
87
  reject!(error);
75
88
  }
76
89
  return promise;
@@ -95,6 +108,8 @@ export async function evaluate<T>(
95
108
  evaluatedItems.map((e) => e.value) as unknown as T,
96
109
  evaluatedItems.flatMap((e) => e.deps),
97
110
  );
111
+ } else if (output instanceof Date) {
112
+ return new Evaluated(output as T);
98
113
  } else if (output && typeof output === "object") {
99
114
  const entries = Object.entries(output);
100
115
  const evaluatedEntries = await Promise.all(
@@ -10,8 +10,8 @@ import {
10
10
  PutBucketTaggingCommand,
11
11
  S3Client,
12
12
  } from "@aws-sdk/client-s3";
13
- import { ignore } from "../../error";
14
- import { Resource } from "../../resource";
13
+ import { ignore } from "../error";
14
+ import { Resource } from "../resource";
15
15
 
16
16
  export interface BucketProps {
17
17
  bucketName: string;
@@ -12,8 +12,8 @@ import {
12
12
  } from "@aws-sdk/client-lambda";
13
13
  import fs from "node:fs";
14
14
  import path from "node:path";
15
- import { ignore } from "../../error";
16
- import { Resource, type Context } from "../../resource";
15
+ import { ignore } from "../error";
16
+ import { type Context, Resource } from "../resource";
17
17
 
18
18
  async function resolveRegion(client: LambdaClient): Promise<string> {
19
19
  const region = client.config.region;
@@ -11,8 +11,8 @@ import {
11
11
  ListPolicyVersionsCommand,
12
12
  NoSuchEntityException,
13
13
  } from "@aws-sdk/client-iam";
14
- import { ignore } from "../../error";
15
- import { Resource } from "../../resource";
14
+ import { ignore } from "../error";
15
+ import { Resource } from "../resource";
16
16
 
17
17
  // Type-safe policy document types
18
18
  export type Effect = "Allow" | "Deny";
@@ -5,7 +5,7 @@ import {
5
5
  GetQueueUrlCommand,
6
6
  SQSClient,
7
7
  } from "@aws-sdk/client-sqs";
8
- import { Resource } from "../../resource";
8
+ import { Resource } from "../resource";
9
9
 
10
10
  export interface QueueProps {
11
11
  queueName: string;
@@ -12,8 +12,8 @@ import {
12
12
  UpdateAssumeRolePolicyCommand,
13
13
  UpdateRoleCommand,
14
14
  } from "@aws-sdk/client-iam";
15
- import { ignore } from "../../error";
16
- import { type Context, Resource } from "../../resource";
15
+ import { ignore } from "../error";
16
+ import { type Context, Resource } from "../resource";
17
17
  import type { PolicyDocument } from "./policy";
18
18
 
19
19
  export interface RoleProps {
@@ -7,8 +7,8 @@ import {
7
7
  ResourceInUseException,
8
8
  ResourceNotFoundException,
9
9
  } from "@aws-sdk/client-dynamodb";
10
- import { ignore } from "../../error";
11
- import { type Context, Resource } from "../../resource";
10
+ import { ignore } from "../error";
11
+ import { type Context, Resource } from "../resource";
12
12
 
13
13
  export interface TableProps {
14
14
  tableName: string;
package/src/destroy.ts CHANGED
@@ -1,10 +1,14 @@
1
- import { defaultStage, stateStore } from "./global";
1
+ import { defaultStage, defaultStateStore, rootScope } from "./global";
2
2
  import type { Output } from "./output";
3
3
  import { Provider, ResourceID, isResource } from "./resource";
4
- import type { State } from "./state";
4
+ import type { Scope } from "./scope";
5
+ import type { State, StateStore } from "./state";
5
6
 
6
- interface DestroyOptions {
7
+ export interface DestroyOptions {
7
8
  stage?: string;
9
+ stateStore?: StateStore;
10
+ scope?: Scope;
11
+ quiet?: boolean;
8
12
  }
9
13
 
10
14
  /**
@@ -20,44 +24,64 @@ export async function destroy<T>(
20
24
  */
21
25
  export async function destroy<T>(
22
26
  stage: string,
27
+ scope: Scope,
23
28
  resourceID: ResourceID,
24
29
  resourceState: State,
25
30
  resourceProvider: Provider,
31
+ stateStore: StateStore,
32
+ options: DestroyOptions,
26
33
  ): Promise<void>;
27
34
 
28
35
  export async function destroy<T>(
29
- ...args: [Output<T>, DestroyOptions?] | [string, ResourceID, State, Provider]
36
+ ...args:
37
+ | [Output<T>, DestroyOptions?]
38
+ | [string, Scope, ResourceID, State, Provider, StateStore, DestroyOptions]
30
39
  ): Promise<void> {
31
40
  let resourceID: ResourceID;
32
41
  let resourceState: State;
33
42
  let resourceProvider: Provider;
34
43
  let stage: string;
35
-
36
- if (args.length === 4) {
44
+ let stateStore: StateStore;
45
+ let scope: Scope | undefined = undefined;
46
+ let options: DestroyOptions | undefined = undefined;
47
+ if (args.length === 7) {
37
48
  stage = args[0];
38
- resourceID = args[1];
39
- resourceState = args[2];
40
- resourceProvider = args[3];
49
+ scope = args[1];
50
+ resourceID = args[2];
51
+ resourceState = args[3];
52
+ resourceProvider = args[4];
53
+ stateStore = args[5];
54
+ options = args[6];
41
55
  } else if (isResource(args[0])) {
42
56
  const resource = args[0];
43
- stage = args[1]?.stage ?? defaultStage;
57
+ // stage = args[1]?.stage ?? defaultStage;
44
58
  resourceID = resource[ResourceID];
59
+ stage = args[1]?.stage ?? defaultStage;
60
+ scope = args[1]?.scope ?? rootScope;
61
+ const statePath = scope.getScopePath(stage);
62
+ stateStore = args[1]?.stateStore ?? new defaultStateStore(statePath);
45
63
  // First destroy all dependencies
46
- const _resourceState = await stateStore.get(stage, resourceID);
64
+ const _resourceState = await stateStore.get(resourceID);
47
65
  if (_resourceState === undefined) {
48
66
  // we have no record of this resource, we must assume it's already deleted
49
67
  return;
50
68
  }
51
69
  resourceState = _resourceState;
52
70
  resourceProvider = resource[Provider];
71
+ options = args[1];
53
72
  } else {
73
+ console.log(args[0]);
54
74
  throw new Error("Not implemented: must handle destroy a Output chain");
55
75
  }
56
76
  await resourceProvider.delete(
57
77
  stage,
78
+ scope,
58
79
  resourceID,
59
80
  resourceState,
60
81
  resourceState.inputs as [],
82
+ options ?? {
83
+ quiet: false,
84
+ },
61
85
  );
62
- await stateStore.delete(stage, resourceID);
86
+ await stateStore.delete(resourceID);
63
87
  }
@@ -2,7 +2,7 @@ import * as esbuild from "esbuild";
2
2
  import crypto from "node:crypto";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
- import { Resource } from "../resource";
5
+ import { Resource } from "./resource";
6
6
 
7
7
  export interface BundleProps {
8
8
  /**
@@ -1,7 +1,8 @@
1
1
  import fs from "node:fs";
2
+ import { unlink } from "node:fs/promises";
2
3
  import path from "node:path";
3
- import { ignore } from "../error";
4
- import { Resource } from "../resource";
4
+ import { ignore } from "./error";
5
+ import { Resource } from "./resource";
5
6
 
6
7
  export class File extends Resource(
7
8
  "fs::File",
@@ -18,28 +19,29 @@ export class File extends Resource(
18
19
 
19
20
  export class Folder extends Resource(
20
21
  "fs::Folder",
21
- async (ctx, dirPath: string) => {
22
+ async (ctx, dirPath: string): Promise<{ path: string }> => {
22
23
  if (ctx.event === "delete") {
23
- if (await ctx.get("isOwned")) {
24
- // only delete the folder if we own it (we created it)
25
- await ignore("ENOENT", () => fs.promises.rmdir(dirPath));
26
- }
24
+ // we just do a best effort attempt
25
+ await ignore(["ENOENT", "ENOTEMPTY"], () => fs.promises.rmdir(dirPath));
27
26
  } else {
28
- try {
29
- await fs.promises.mkdir(dirPath, { recursive: true });
30
- } catch (error: any) {
31
- if (error.code === "EEXIST") {
32
- // record that we own the directory
33
- await ctx.set("isOwned", true);
34
- } else {
35
- throw error;
36
- }
37
- }
27
+ await ignore("EEXIST", () =>
28
+ fs.promises.mkdir(dirPath, { recursive: true }),
29
+ );
38
30
  }
39
- return dirPath;
31
+ return { path: dirPath };
40
32
  },
41
33
  ) {
42
34
  public file(filePath: string, content: string) {
43
35
  return new File(path.basename(filePath), filePath, content);
44
36
  }
45
37
  }
38
+
39
+ export async function rm(filePath: string) {
40
+ try {
41
+ await unlink(filePath);
42
+ } catch (error) {
43
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
44
+ throw error;
45
+ }
46
+ }
47
+ }
package/src/global.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import type { Provider, Resource, ResourceID, ResourceType } from "./resource";
4
- import { defaultStore, type StateStore } from "./state";
3
+ import type { Provider, ResourceType } from "./resource";
4
+ import { Scope } from "./scope";
5
+ import { FileSystemStateStore } from "./state";
5
6
 
6
7
  export interface Config {
7
8
  defaultStage?: string;
8
- stateStore?: StateStore;
9
+ stateStore?: new (dir: string) => FileSystemStateStore;
9
10
  }
10
11
 
11
12
  const alchemy = path.resolve(process.cwd(), "alchemy.ts");
@@ -43,20 +44,14 @@ const _stage = process.env.ALCHEMY_STAGE ?? process.env.USER ?? "dev";
43
44
 
44
45
  export const config: Config = _config ?? {
45
46
  defaultStage: _stage,
46
- stateStore: defaultStore,
47
+ stateStore: FileSystemStateStore,
47
48
  };
48
49
 
49
50
  export const defaultStage = config.defaultStage ?? _stage;
50
51
 
51
- export const stateStore = config.stateStore ?? defaultStore;
52
+ export const defaultStateStore = config.stateStore ?? FileSystemStateStore;
52
53
 
53
- export const nodes = new Map<
54
- ResourceID,
55
- {
56
- provider: Provider<any, any[], any>;
57
- resource: Resource<any, any>;
58
- }
59
- >();
54
+ export const rootScope = new Scope(null);
60
55
 
61
56
  export const providers = new Map<
62
57
  ResourceType,
@@ -68,5 +63,3 @@ export const deletions: {
68
63
  data: Record<string, any>;
69
64
  inputs: any[];
70
65
  }[] = [];
71
-
72
- await stateStore.init?.();
package/src/index.ts CHANGED
@@ -1,6 +1,15 @@
1
+ export * from "./$.ts";
1
2
  export * from "./alchemize";
3
+ export * from "./error";
2
4
  export * from "./global";
5
+ export * from "./input";
3
6
  export * from "./output";
4
- export * from "./resource";
7
+ export {
8
+ Resource,
9
+ type Context,
10
+ type CreateContext,
11
+ type DeleteContext,
12
+ type UpdateContext,
13
+ } from "./resource";
5
14
 
6
- export type { StateStore } from "./state";
15
+ export type { FileSystemStateStore as StateStore } from "./state";
package/src/input.ts CHANGED
@@ -4,7 +4,7 @@ export type Inputs<P extends any[]> = P extends [infer First, ...infer Rest]
4
4
  ? [Input<First>, ...Inputs<Rest>]
5
5
  : [];
6
6
 
7
- export type Input<T> =
7
+ export type Input<T = any> =
8
8
  | T
9
9
  | Output<T>
10
10
  | (T extends any[] ? Inputs<T> : never)
@@ -0,0 +1,125 @@
1
+ import { tool } from "ai";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { z } from "zod";
5
+ import {
6
+ type FileContext,
7
+ type ModelId,
8
+ dependenciesAsMessages,
9
+ generateText,
10
+ resolveModel,
11
+ } from "../agent";
12
+ import { Prompts } from "../agent/prompts";
13
+ import { rm } from "../fs";
14
+ import { extractMarkdown } from "../markdown/extract";
15
+ import { type Context, Resource } from "../resource";
16
+
17
+ export interface DesignInput {
18
+ prompt: string;
19
+ path: string;
20
+ deps?: any[];
21
+ model?: ModelId;
22
+ dependencies: FileContext[];
23
+ }
24
+
25
+ export interface DesignOutput {
26
+ path: string;
27
+ content: string;
28
+ }
29
+
30
+ export class Design extends Resource(
31
+ "design",
32
+ async (ctx: Context<DesignOutput>, props: DesignInput) => {
33
+ if (ctx.event === "delete") {
34
+ await rm(props.path);
35
+ return;
36
+ }
37
+ const model = await resolveModel(props.model ?? "gpt-4o");
38
+
39
+ const importFile = tool({
40
+ description: "Imports a relevant file into context",
41
+ parameters: z.object({
42
+ file: z.string(),
43
+ }),
44
+ execute: async ({ file }) => fs.readFile(file, "utf-8"),
45
+ });
46
+
47
+ let existingContent: string | undefined = undefined;
48
+ if (ctx.event === "update") {
49
+ if (props.path) {
50
+ try {
51
+ existingContent = await fs.readFile(props.path, "utf-8");
52
+ } catch {
53
+ // File doesn't exist yet, continue with empty content
54
+ }
55
+ } else {
56
+ existingContent = ctx.output.content;
57
+ }
58
+ }
59
+
60
+ console.log(
61
+ ctx.event === "create" ? "Designing" : "Revising design",
62
+ ":",
63
+ props.path,
64
+ );
65
+
66
+ const result = await generateText({
67
+ model,
68
+ // tools: {
69
+ // import: importFile,
70
+ // },
71
+ providerOptions:
72
+ props.model === "o3-mini"
73
+ ? {
74
+ openai: {
75
+ reasoningEffort: "high",
76
+ },
77
+ }
78
+ : undefined,
79
+ messages: [
80
+ {
81
+ role: "system",
82
+ content: Prompts.program,
83
+ },
84
+ ...dependenciesAsMessages(props.dependencies),
85
+ ...(existingContent
86
+ ? [
87
+ {
88
+ role: "user" as const,
89
+ content: `Here is the prior version of ${props.path}:\n${existingContent}`,
90
+ },
91
+ {
92
+ role: "assistant" as const,
93
+ content:
94
+ "Thanks, I'll make sure to consider this when producing the next revision.",
95
+ },
96
+ ]
97
+ : []),
98
+ {
99
+ role: "user",
100
+ content: props.prompt,
101
+ },
102
+ {
103
+ role: "user",
104
+ content: [
105
+ `Make sure to list each of the files that need to be implemented and`,
106
+ "and to follow best practices in term of separation of concerns.",
107
+ "Ideally, each component has its own dedicated file.",
108
+ "Each file should be referenced using markdown links, e.g. [file.ts](./file.ts).",
109
+ ].join(" "),
110
+ },
111
+ ],
112
+ });
113
+
114
+ const content = extractMarkdown(result.text);
115
+
116
+ await fs.mkdir(path.dirname(props.path), { recursive: true });
117
+
118
+ await fs.writeFile(props.path, content);
119
+
120
+ return {
121
+ path: props.path,
122
+ content,
123
+ };
124
+ },
125
+ ) {}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Extracts markdown content from a code block or returns the full content
3
+ * @param text Response text that may contain markdown code blocks
4
+ * @returns The markdown content from code block if found, otherwise returns the full text
5
+ */
6
+ export function extractMarkdown(text: string): string {
7
+ const matches = text.match(/```md\n([\s\S]*?)```/);
8
+ return matches?.[1]?.trim() ?? text.trim();
9
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./design";
2
+ export * from "./extract";
3
+ export * from "./requirements";