alchemy 0.1.2 → 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.
- package/README.md +8 -39
- package/package.json +12 -8
- package/src/$.ts +36 -0
- package/src/agent/ai.ts +215 -0
- package/src/{components/agent → agent}/anthropic.ts +6 -2
- package/src/agent/dependencies.ts +62 -0
- package/src/agent/file-context.ts +8 -0
- package/src/agent/index.ts +9 -0
- package/src/agent/model.ts +39 -0
- package/src/agent/module.ts +418 -0
- package/src/{components/agent → agent}/openai.ts +6 -0
- package/src/agent/program.md +38 -0
- package/src/agent/program.ts +37 -0
- package/src/agent/prompts.ts +20 -0
- package/src/alchemize.ts +45 -14
- package/src/apply.ts +25 -10
- package/src/{components/aws → aws}/bucket.ts +2 -2
- package/src/{components/aws → aws}/function.ts +2 -2
- package/src/{components/aws → aws}/policy.ts +2 -2
- package/src/{components/aws → aws}/queue.ts +1 -1
- package/src/{components/aws → aws}/role.ts +2 -2
- package/src/{components/aws → aws}/table.ts +2 -2
- package/src/destroy.ts +36 -12
- package/src/{components/esbuild.ts → esbuild.ts} +1 -1
- package/src/{components/fs.ts → fs.ts} +20 -18
- package/src/global.ts +7 -14
- package/src/index.ts +11 -2
- package/src/input.ts +1 -1
- package/src/markdown/design.ts +125 -0
- package/src/markdown/extract.ts +9 -0
- package/src/markdown/index.ts +3 -0
- package/src/{components/agent → markdown}/requirements.ts +53 -29
- package/src/output.ts +6 -0
- package/src/resource.ts +208 -75
- package/src/scope.ts +56 -0
- package/src/slug.ts +3 -0
- package/src/state.ts +66 -38
- package/src/typescript/check-omission.ts +47 -0
- package/src/typescript/debug-type-errors.ts +42 -0
- package/src/typescript/extract.ts +9 -0
- package/src/{components/agent → typescript}/index.ts +3 -1
- package/src/typescript/install-packages.ts +50 -0
- package/src/{components/agent → typescript}/package.ts +44 -27
- package/src/typescript/repair-omissions.ts +31 -0
- package/src/typescript/repair.ts +45 -0
- package/src/{components/agent → typescript}/tsconfig.ts +37 -17
- package/src/typescript/typescript.ts +217 -0
- package/src/typescript/validate.ts +73 -0
- package/test/scope.test.ts +67 -0
- package/src/components/agent/agent.ts +0 -45
- package/src/components/agent/model.ts +0 -22
- package/src/components/agent/typescript.ts +0 -177
- /package/src/{components/agent → agent}/scrape.ts +0 -0
- /package/src/{components/aws → aws}/index.ts +0 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { dirname, resolve } from "path";
|
|
2
|
+
import * as ts from "typescript";
|
|
3
|
+
|
|
4
|
+
export interface TypeCheckOptions {
|
|
5
|
+
tsconfigPath?: string;
|
|
6
|
+
projectRoot?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validates TypeScript code and returns formatted error messages if there are any type errors
|
|
11
|
+
* @returns Formatted error message if there are errors, undefined if valid
|
|
12
|
+
*/
|
|
13
|
+
export async function validateTypeScript(
|
|
14
|
+
filePath: string,
|
|
15
|
+
options: TypeCheckOptions,
|
|
16
|
+
): Promise<string | undefined> {
|
|
17
|
+
const projectRoot = options.projectRoot ?? dirname(filePath);
|
|
18
|
+
const tsconfigPath =
|
|
19
|
+
options.tsconfigPath ?? resolve(projectRoot, "tsconfig.json");
|
|
20
|
+
|
|
21
|
+
// Load tsconfig.json if it exists
|
|
22
|
+
let compilerOptions: ts.CompilerOptions = {
|
|
23
|
+
target: ts.ScriptTarget.ESNext,
|
|
24
|
+
module: ts.ModuleKind.ESNext,
|
|
25
|
+
strict: true,
|
|
26
|
+
esModuleInterop: true,
|
|
27
|
+
skipLibCheck: true,
|
|
28
|
+
noEmit: true,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (ts.sys.fileExists(tsconfigPath)) {
|
|
32
|
+
const { config, error } = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
|
|
33
|
+
if (error) {
|
|
34
|
+
throw new Error(`Error reading tsconfig.json: ${error.messageText}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const { options: parsedOptions, errors } = ts.parseJsonConfigFileContent(
|
|
38
|
+
config,
|
|
39
|
+
ts.sys,
|
|
40
|
+
projectRoot,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (errors.length) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Error parsing tsconfig.json: ${errors.map((e) => e.messageText).join("\n")}`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
compilerOptions = parsedOptions;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Create program
|
|
53
|
+
const program = ts.createProgram({
|
|
54
|
+
rootNames: [filePath],
|
|
55
|
+
options: compilerOptions,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Get diagnostics
|
|
59
|
+
const diagnostics = [
|
|
60
|
+
...program.getSemanticDiagnostics(),
|
|
61
|
+
...program.getSyntacticDiagnostics(),
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
if (diagnostics.length > 0) {
|
|
65
|
+
return ts.formatDiagnosticsWithColorAndContext(diagnostics, {
|
|
66
|
+
getCanonicalFileName: (path) => path,
|
|
67
|
+
getCurrentDirectory: () => projectRoot,
|
|
68
|
+
getNewLine: () => ts.sys.newLine,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { apply } from "../src/apply";
|
|
3
|
+
import type { PolicyDocument } from "../src/components/aws/policy";
|
|
4
|
+
import { Role } from "../src/components/aws/role";
|
|
5
|
+
import { File } from "../src/components/fs";
|
|
6
|
+
import { destroy } from "../src/destroy";
|
|
7
|
+
import { rootScope } from "../src/global";
|
|
8
|
+
import { type Context, Resource } from "../src/resource";
|
|
9
|
+
import { Scope, getScope, withScope } from "../src/scope";
|
|
10
|
+
|
|
11
|
+
describe("Scope", () => {
|
|
12
|
+
it("should maintain scope context and track resources", async () => {
|
|
13
|
+
const testScope = new Scope("test");
|
|
14
|
+
|
|
15
|
+
await withScope(testScope, async () => {
|
|
16
|
+
new File("test-file", "test.txt", "Hello World");
|
|
17
|
+
|
|
18
|
+
const currentScope = getScope();
|
|
19
|
+
expect(currentScope).toEqual(testScope);
|
|
20
|
+
});
|
|
21
|
+
expect(testScope.nodes.size).toBe(1);
|
|
22
|
+
expect(testScope).toBe(testScope);
|
|
23
|
+
expect(testScope).not.toBe(rootScope);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should handle nested resources with AWS Role", async () => {
|
|
27
|
+
class ServiceResources extends Resource(
|
|
28
|
+
"service-resources",
|
|
29
|
+
async (
|
|
30
|
+
ctx: Context<{ roleArn: string }>,
|
|
31
|
+
props: { roleName: string },
|
|
32
|
+
) => {
|
|
33
|
+
const assumeRolePolicy: PolicyDocument = {
|
|
34
|
+
Version: "2012-10-17",
|
|
35
|
+
Statement: [
|
|
36
|
+
{
|
|
37
|
+
Effect: "Allow",
|
|
38
|
+
Principal: {
|
|
39
|
+
Service: "lambda.amazonaws.com",
|
|
40
|
+
},
|
|
41
|
+
Action: "sts:AssumeRole",
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const role = new Role(`role`, {
|
|
47
|
+
roleName: props.roleName,
|
|
48
|
+
assumeRolePolicy,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
roleArn: role.arn,
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
) {}
|
|
56
|
+
|
|
57
|
+
const service = new ServiceResources("test-service", {
|
|
58
|
+
roleName: "alchemy-test-scope-role",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Apply the parent resource which should create the role
|
|
62
|
+
const output = await apply(service);
|
|
63
|
+
|
|
64
|
+
// Clean up
|
|
65
|
+
await destroy(service);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import type { ZodType } from "zod";
|
|
2
|
-
import { type Context, type Provider, Resource } from "../../resource";
|
|
3
|
-
|
|
4
|
-
export function Agent<Type extends string, TInput, TOutput>(
|
|
5
|
-
type: Type,
|
|
6
|
-
{
|
|
7
|
-
description,
|
|
8
|
-
input,
|
|
9
|
-
output,
|
|
10
|
-
}: {
|
|
11
|
-
description: string;
|
|
12
|
-
input: ZodType<TInput>;
|
|
13
|
-
output: ZodType<TOutput>;
|
|
14
|
-
},
|
|
15
|
-
handler: (ctx: Context<TOutput>, props: TInput) => Promise<TOutput | void>,
|
|
16
|
-
): Provider<Type, [TInput], Awaited<TOutput>> & {
|
|
17
|
-
description: string;
|
|
18
|
-
input: ZodType<TInput>;
|
|
19
|
-
output: ZodType<TOutput>;
|
|
20
|
-
} {
|
|
21
|
-
// @ts-expect-error - we don't care that the props are not statically known, they will be visible outside this scope
|
|
22
|
-
return class Agent extends Resource(
|
|
23
|
-
type,
|
|
24
|
-
async (ctx: Context<TOutput>, props: TInput): Promise<TOutput | void> => {
|
|
25
|
-
// Validate input
|
|
26
|
-
const validatedInput = input.parse(props);
|
|
27
|
-
|
|
28
|
-
// Call handler with validated input
|
|
29
|
-
const result = await handler(ctx, validatedInput);
|
|
30
|
-
|
|
31
|
-
if (ctx.event === "delete") {
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Validate output
|
|
36
|
-
const validatedOutput = output.parse(result);
|
|
37
|
-
|
|
38
|
-
return validatedOutput;
|
|
39
|
-
},
|
|
40
|
-
) {
|
|
41
|
-
static readonly description = description;
|
|
42
|
-
static readonly input = input;
|
|
43
|
-
static readonly output = output;
|
|
44
|
-
};
|
|
45
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { type AnthropicModelId, isAnthropicModel } from "./anthropic";
|
|
2
|
-
import { type OpenAIChatModelId, isOpenAIModel } from "./openai";
|
|
3
|
-
|
|
4
|
-
export type ModelId = OpenAIChatModelId | AnthropicModelId;
|
|
5
|
-
|
|
6
|
-
export async function resolveModel(modelId: ModelId) {
|
|
7
|
-
if (isOpenAIModel(modelId)) {
|
|
8
|
-
const { openai } = await import("@ai-sdk/openai");
|
|
9
|
-
return {
|
|
10
|
-
type: "openai" as const,
|
|
11
|
-
model: openai(modelId as OpenAIChatModelId),
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
if (isAnthropicModel(modelId)) {
|
|
15
|
-
const { anthropic } = await import("@ai-sdk/anthropic");
|
|
16
|
-
return {
|
|
17
|
-
type: "anthropic" as const,
|
|
18
|
-
model: anthropic(modelId as AnthropicModelId),
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
throw new Error(`Unsupported model ID: ${modelId}`);
|
|
22
|
-
}
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import type { CoreMessage } from "ai";
|
|
2
|
-
import { generateText } from "ai";
|
|
3
|
-
import { mkdir, unlink, writeFile } from "fs/promises";
|
|
4
|
-
import { dirname } from "path";
|
|
5
|
-
import { z } from "zod";
|
|
6
|
-
import { Agent } from "./agent";
|
|
7
|
-
import { resolveModel } from "./model";
|
|
8
|
-
|
|
9
|
-
export type CodeProps = z.infer<typeof CodeProps>;
|
|
10
|
-
|
|
11
|
-
export type File = z.infer<typeof File>;
|
|
12
|
-
export const File = z.object({
|
|
13
|
-
path: z.string(),
|
|
14
|
-
content: z.string(),
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
const CodeProps = z.object({
|
|
18
|
-
/**
|
|
19
|
-
* The ID of the model to use for generating TypeScript code
|
|
20
|
-
* @default "gpt-4o"
|
|
21
|
-
*/
|
|
22
|
-
modelId: z.string().optional(),
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* The name of the file to write the code to
|
|
26
|
-
*/
|
|
27
|
-
path: z.string(),
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* The requirements for the TypeScript file
|
|
31
|
-
*/
|
|
32
|
-
requirements: z.string(),
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* List of other code files that it depends on
|
|
36
|
-
*/
|
|
37
|
-
dependencies: z.array(File).optional(),
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Temperature setting for model generation (higher = more creative, lower = more focused)
|
|
41
|
-
* @default 0.7
|
|
42
|
-
*/
|
|
43
|
-
temperature: z.number().optional(),
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
export type CodeOutput = z.infer<typeof CodeOutput>;
|
|
47
|
-
export const CodeOutput = File;
|
|
48
|
-
|
|
49
|
-
export class TypeScriptFile extends Agent(
|
|
50
|
-
"code::TypeScriptFile",
|
|
51
|
-
{
|
|
52
|
-
description:
|
|
53
|
-
"This Agent is responsible for generating TypeScript code based on requirements and context from other code files.",
|
|
54
|
-
input: CodeProps,
|
|
55
|
-
output: CodeOutput,
|
|
56
|
-
},
|
|
57
|
-
async (ctx, props) => {
|
|
58
|
-
if (ctx.event === "delete") {
|
|
59
|
-
await unlink(props.path);
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Get the appropriate model based on the ID
|
|
64
|
-
const { model } = await resolveModel(props.modelId ?? "gpt-4o");
|
|
65
|
-
|
|
66
|
-
// Generate the TypeScript code with retries
|
|
67
|
-
const content = await generateCodeWithRetries(model, props);
|
|
68
|
-
|
|
69
|
-
// Ensure the directory exists
|
|
70
|
-
await mkdir(dirname(props.path), { recursive: true });
|
|
71
|
-
|
|
72
|
-
// Write the TypeScript file
|
|
73
|
-
await writeFile(props.path, content);
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
path: props.path,
|
|
77
|
-
content: content,
|
|
78
|
-
};
|
|
79
|
-
},
|
|
80
|
-
) {}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Attempts to generate valid TypeScript code with retries
|
|
84
|
-
*/
|
|
85
|
-
async function generateCodeWithRetries(
|
|
86
|
-
model: any,
|
|
87
|
-
props: CodeProps,
|
|
88
|
-
maxAttempts = 3,
|
|
89
|
-
): Promise<string> {
|
|
90
|
-
const baseMessages: CoreMessage[] = [
|
|
91
|
-
{
|
|
92
|
-
role: "system",
|
|
93
|
-
content:
|
|
94
|
-
"You are an expert TypeScript developer that generates clean, well-documented code following best practices. " +
|
|
95
|
-
"Your response must follow this exact format:\n" +
|
|
96
|
-
"1. A single sentence describing what the code will do\n" +
|
|
97
|
-
"2. A single TypeScript code block surrounded by ```ts and ``` tags\n" +
|
|
98
|
-
"Do not include any other explanations or multiple code blocks.",
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
role: "user",
|
|
102
|
-
content: `Please generate TypeScript code based on the following specifications:
|
|
103
|
-
|
|
104
|
-
Requirements:
|
|
105
|
-
${props.requirements}
|
|
106
|
-
|
|
107
|
-
${
|
|
108
|
-
props.dependencies?.length
|
|
109
|
-
? `Context (other code files):
|
|
110
|
-
${props.dependencies.map((dep) => dep.content).join("\n\n")}`
|
|
111
|
-
: ""
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
The code should:
|
|
115
|
-
1. Be well-documented with JSDoc comments
|
|
116
|
-
2. Follow TypeScript best practices
|
|
117
|
-
3. Include proper type definitions
|
|
118
|
-
4. Be ready to use with the provided context files
|
|
119
|
-
|
|
120
|
-
Your response MUST follow this format:
|
|
121
|
-
- One sentence summary of what the code will do
|
|
122
|
-
- Single TypeScript code block surrounded by \`\`\`ts and \`\`\` tags
|
|
123
|
-
- No other explanations or multiple code blocks`,
|
|
124
|
-
},
|
|
125
|
-
];
|
|
126
|
-
|
|
127
|
-
const messages = [...baseMessages];
|
|
128
|
-
|
|
129
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
130
|
-
const { text } = await generateText({
|
|
131
|
-
model,
|
|
132
|
-
temperature: Math.max(
|
|
133
|
-
0.1,
|
|
134
|
-
(props.temperature ?? 0.7) * (1 - attempt * 0.2),
|
|
135
|
-
), // Reduce temperature with each attempt
|
|
136
|
-
messages,
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
const code = extractTypeScriptCode(text);
|
|
140
|
-
if (code) {
|
|
141
|
-
return code;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (attempt === maxAttempts) {
|
|
145
|
-
throw new Error(
|
|
146
|
-
"Failed to generate valid TypeScript code after maximum attempts",
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Add the failed attempt and correction request to the message chain
|
|
151
|
-
messages.push(
|
|
152
|
-
{
|
|
153
|
-
role: "assistant",
|
|
154
|
-
content: text,
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
role: "user",
|
|
158
|
-
content:
|
|
159
|
-
"Your response did not match the required format. Please provide your response with exactly one sentence summary" +
|
|
160
|
-
" followed by a single TypeScript code block surrounded by ```ts and ``` tags. No other text or explanations.",
|
|
161
|
-
},
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// This should never be reached due to the throw above
|
|
166
|
-
throw new Error("Unexpected code generation failure");
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Extracts TypeScript code from a markdown code block
|
|
171
|
-
* @param text Response text that may contain markdown
|
|
172
|
-
* @returns The TypeScript code if found, undefined otherwise
|
|
173
|
-
*/
|
|
174
|
-
function extractTypeScriptCode(text: string): string | undefined {
|
|
175
|
-
const matches = text.match(/```(?:ts|typescript)\n([\s\S]*?)```/);
|
|
176
|
-
return matches?.[1]?.trim();
|
|
177
|
-
}
|
|
File without changes
|
|
File without changes
|