editcodewithai 0.0.1 → 1.0.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.
- package/README.md +129 -27
- package/dist/fileUtils.d.ts +14 -0
- package/dist/fileUtils.js +66 -0
- package/dist/index.d.ts +3 -19
- package/dist/index.js +19 -139
- package/dist/metadata.d.ts +12 -0
- package/dist/metadata.js +51 -0
- package/dist/prompt.d.ts +8 -0
- package/dist/prompt.js +31 -0
- package/dist/types.d.ts +22 -0
- package/dist/types.js +2 -0
- package/package.json +24 -8
package/README.md
CHANGED
|
@@ -1,52 +1,118 @@
|
|
|
1
1
|
# editcodewithai
|
|
2
|
-
Edit Code With AI
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
A lightweight, flexible library for AI-powered code editing.
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
## Overview
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
`editcodewithai` is a JavaScript/TypeScript library that enables AI-powered code editing in your applications. It provides a simple interface to send code files and instructions to an LLM (Large Language Model) and receive edited code in return.
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
# Clone the repository
|
|
12
|
-
git clone https://github.com/yourusername/editcodewithai.git
|
|
9
|
+
The library is designed to be model-agnostic, allowing you to use any LLM provider while handling the prompt engineering, file parsing, and response processing for you.
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
cd editcodewithai
|
|
11
|
+
## Installation
|
|
16
12
|
|
|
17
|
-
|
|
18
|
-
npm install
|
|
13
|
+
```bash
|
|
14
|
+
npm install editcodewithai
|
|
19
15
|
```
|
|
20
16
|
|
|
21
17
|
## Usage
|
|
22
18
|
|
|
23
19
|
```typescript
|
|
24
|
-
import { performAiEdit } from
|
|
20
|
+
import { performAiEdit } from "editcodewithai";
|
|
21
|
+
import { VizFiles } from "@vizhub/viz-types";
|
|
22
|
+
|
|
23
|
+
// Define your LLM function that will process the prompt
|
|
24
|
+
const myLlmFunction = async (prompt: string) => {
|
|
25
|
+
// Call your preferred LLM API here
|
|
26
|
+
// This example assumes using OpenRouter
|
|
27
|
+
const response = await fetch(
|
|
28
|
+
"https://openrouter.ai/api/v1/chat/completions",
|
|
29
|
+
{
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: {
|
|
32
|
+
"Content-Type": "application/json",
|
|
33
|
+
Authorization: `Bearer ${apiKey}`,
|
|
34
|
+
},
|
|
35
|
+
body: JSON.stringify({
|
|
36
|
+
model: "openai/gpt-4",
|
|
37
|
+
messages: [{ role: "user", content: prompt }],
|
|
38
|
+
}),
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const data = await response.json();
|
|
43
|
+
return {
|
|
44
|
+
content: data.choices[0].message.content,
|
|
45
|
+
generationId: data.id,
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Your files
|
|
50
|
+
const files: VizFiles = {
|
|
51
|
+
file1: {
|
|
52
|
+
name: "index.js",
|
|
53
|
+
text: "console.log('Hello world');",
|
|
54
|
+
},
|
|
55
|
+
};
|
|
25
56
|
|
|
57
|
+
// Perform the AI edit
|
|
26
58
|
const result = await performAiEdit({
|
|
27
59
|
prompt: "Update the code to use async/await",
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// Your files object
|
|
31
|
-
},
|
|
60
|
+
files: files,
|
|
61
|
+
llmFunction: myLlmFunction,
|
|
32
62
|
apiKey: "your-openrouter-api-key",
|
|
33
|
-
baseURL: "https://openrouter.ai/api/v1"
|
|
34
63
|
});
|
|
64
|
+
|
|
65
|
+
console.log(result.changedFiles);
|
|
35
66
|
```
|
|
36
67
|
|
|
37
|
-
|
|
38
|
-
|
|
68
|
+
## API Reference
|
|
69
|
+
|
|
70
|
+
### performAiEdit(params)
|
|
71
|
+
|
|
72
|
+
The main function that processes files with an AI model and returns edited code.
|
|
73
|
+
|
|
74
|
+
#### Parameters
|
|
75
|
+
|
|
76
|
+
| Parameter | Type | Description |
|
|
77
|
+
| ------------- | ------------- | ----------------------------------------------------------------- |
|
|
78
|
+
| `prompt` | `string` | Instructions for the AI on how to modify the code |
|
|
79
|
+
| `files` | `VizFiles` | Object containing file information (see below) |
|
|
80
|
+
| `llmFunction` | `LlmFunction` | Function that sends the prompt to an LLM and returns the response |
|
|
81
|
+
| `apiKey` | `string` | OpenRouter API key for retrieving cost metadata |
|
|
82
|
+
|
|
83
|
+
The `VizFiles` type is a map of file IDs to file objects, where each file object has:
|
|
84
|
+
|
|
85
|
+
- `name`: The filename (e.g., "index.js")
|
|
39
86
|
- `text`: The file contents as a string
|
|
40
87
|
|
|
41
|
-
The function returns:
|
|
42
|
-
|
|
43
|
-
- `
|
|
44
|
-
- `
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
88
|
+
The `LlmFunction` type is a function that takes a prompt string and returns a Promise with:
|
|
89
|
+
|
|
90
|
+
- `content`: The LLM's response text
|
|
91
|
+
- `generationId`: A unique ID for the generation (used for cost tracking)
|
|
92
|
+
|
|
93
|
+
#### Return Value
|
|
94
|
+
|
|
95
|
+
The function returns an object with:
|
|
96
|
+
|
|
97
|
+
| Property | Type | Description |
|
|
98
|
+
| ------------------------ | ---------- | ------------------------------------------ |
|
|
99
|
+
| `changedFiles` | `VizFiles` | Updated files with AI modifications |
|
|
100
|
+
| `openRouterGenerationId` | `string` | ID of the generation from the LLM provider |
|
|
101
|
+
| `upstreamCostCents` | `number` | Cost of the API call in cents |
|
|
102
|
+
| `provider` | `string` | The AI provider used (e.g., "openai") |
|
|
103
|
+
| `inputTokens` | `number` | Number of input tokens used |
|
|
104
|
+
| `outputTokens` | `number` | Number of output tokens generated |
|
|
105
|
+
| `promptTemplateVersion` | `number` | Version of the prompt template used |
|
|
106
|
+
|
|
107
|
+
### File Operations
|
|
108
|
+
|
|
109
|
+
The library handles several file operations automatically:
|
|
48
110
|
|
|
49
|
-
|
|
111
|
+
- **Updating existing files**: When the AI modifies a file's content
|
|
112
|
+
- **Creating new files**: When the AI suggests new files to add
|
|
113
|
+
- **Deleting files**: When the AI returns empty content for a file
|
|
114
|
+
|
|
115
|
+
## Similar Projects
|
|
50
116
|
|
|
51
117
|
- **Aider**: An AI pair programming tool that integrates with your terminal to assist in code editing within your local git repository. [https://aider.chat/](https://aider.chat/)
|
|
52
118
|
|
|
@@ -61,3 +127,39 @@ The function returns:
|
|
|
61
127
|
- **Void**: An open-source alternative to proprietary AI code editors, offering AI-assisted coding features while prioritizing user privacy and control. [https://void.dev/](https://void.dev/)
|
|
62
128
|
|
|
63
129
|
- **Cody**: An advanced AI coding assistant developed by Sourcegraph, integrating seamlessly with popular IDEs to provide features like AI-driven chat, code autocompletion, and inline editing. [https://github.com/sourcegraph/cody](https://github.com/sourcegraph/cody)
|
|
130
|
+
|
|
131
|
+
## Contributing
|
|
132
|
+
|
|
133
|
+
To contribute to this project:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
# Clone the repository
|
|
137
|
+
git clone https://github.com/yourusername/editcodewithai.git
|
|
138
|
+
|
|
139
|
+
# Navigate to project directory
|
|
140
|
+
cd editcodewithai
|
|
141
|
+
|
|
142
|
+
# Install dependencies
|
|
143
|
+
npm install
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Run tests to ensure everything is working correctly:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
npm test
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Please submit pull requests with clear descriptions of changes and ensure all tests pass. Protocol for wrapping up a PR:
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
npm test
|
|
156
|
+
npm run typecheck
|
|
157
|
+
npm run prettier
|
|
158
|
+
# Verify the README is up to date
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Please create an issue first before creating a PR to discuss the changes you want to make. This helps ensure that your contributions align with the project's goals and vision.
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { VizFiles, VizFile, FileCollection } from "@vizhub/viz-types";
|
|
2
|
+
/**
|
|
3
|
+
* If the LLM outputs empty text for a file, we interpret this
|
|
4
|
+
* as a request to delete the file.
|
|
5
|
+
*/
|
|
6
|
+
export declare function shouldDeleteFile(file?: VizFile): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Processes files for the prompt by truncating large files
|
|
9
|
+
*/
|
|
10
|
+
export declare function prepareFilesForPrompt(files: VizFiles): FileCollection;
|
|
11
|
+
/**
|
|
12
|
+
* Merges original files with changes from the LLM
|
|
13
|
+
*/
|
|
14
|
+
export declare function mergeFileChanges(originalFiles: VizFiles, parsedFiles: FileCollection): VizFiles;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.shouldDeleteFile = shouldDeleteFile;
|
|
4
|
+
exports.prepareFilesForPrompt = prepareFilesForPrompt;
|
|
5
|
+
exports.mergeFileChanges = mergeFileChanges;
|
|
6
|
+
const viz_utils_1 = require("@vizhub/viz-utils");
|
|
7
|
+
/**
|
|
8
|
+
* If the LLM outputs empty text for a file, we interpret this
|
|
9
|
+
* as a request to delete the file.
|
|
10
|
+
*/
|
|
11
|
+
function shouldDeleteFile(file) {
|
|
12
|
+
if (!file)
|
|
13
|
+
return false;
|
|
14
|
+
return file.text.trim() === "";
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Processes files for the prompt by truncating large files
|
|
18
|
+
*/
|
|
19
|
+
function prepareFilesForPrompt(files) {
|
|
20
|
+
const result = {};
|
|
21
|
+
Object.values(files).forEach((file) => {
|
|
22
|
+
// Example: truncate large files, etc.
|
|
23
|
+
result[file.name] = file.text
|
|
24
|
+
.split("\n")
|
|
25
|
+
.slice(0, file.name.endsWith(".csv") || file.name.endsWith(".json") ? 50 : 500)
|
|
26
|
+
.map((line) => line.slice(0, 200))
|
|
27
|
+
.join("\n");
|
|
28
|
+
});
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Merges original files with changes from the LLM
|
|
33
|
+
*/
|
|
34
|
+
function mergeFileChanges(originalFiles, parsedFiles) {
|
|
35
|
+
// Start with existing files
|
|
36
|
+
let changedFiles = Object.keys(originalFiles).reduce((acc, fileId) => {
|
|
37
|
+
const original = originalFiles[fileId];
|
|
38
|
+
const changedText = parsedFiles[original.name];
|
|
39
|
+
const changedFile = changedText !== undefined
|
|
40
|
+
? { name: original.name, text: changedText }
|
|
41
|
+
: undefined;
|
|
42
|
+
if (shouldDeleteFile(changedFile)) {
|
|
43
|
+
// Exclude from new set
|
|
44
|
+
return acc;
|
|
45
|
+
}
|
|
46
|
+
// If changedFile is present, use it; otherwise use original
|
|
47
|
+
acc[fileId] = {
|
|
48
|
+
...original,
|
|
49
|
+
text: changedFile ? changedFile.text : original.text,
|
|
50
|
+
};
|
|
51
|
+
return acc;
|
|
52
|
+
}, {});
|
|
53
|
+
// Handle newly-created files
|
|
54
|
+
Object.entries(parsedFiles).forEach(([fileName, fileText]) => {
|
|
55
|
+
const existingFile = Object.values(changedFiles).find((file) => file.name === fileName);
|
|
56
|
+
// If no existing file and not empty => it's a new file
|
|
57
|
+
if (!existingFile && fileText.trim() !== "") {
|
|
58
|
+
const newFileId = (0, viz_utils_1.generateVizFileId)();
|
|
59
|
+
changedFiles[newFileId] = {
|
|
60
|
+
name: fileName,
|
|
61
|
+
text: fileText,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
return changedFiles;
|
|
66
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,25 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export interface PerformAiEditParams {
|
|
3
|
-
prompt: string;
|
|
4
|
-
modelName: string;
|
|
5
|
-
files: VizFiles;
|
|
6
|
-
apiKey: string;
|
|
7
|
-
baseURL: string | undefined;
|
|
8
|
-
}
|
|
9
|
-
export interface PerformAiEditResult {
|
|
10
|
-
changedFiles: VizFiles;
|
|
11
|
-
openRouterGenerationId: string;
|
|
12
|
-
upstreamCostCents: number;
|
|
13
|
-
provider: string;
|
|
14
|
-
inputTokens: number;
|
|
15
|
-
outputTokens: number;
|
|
16
|
-
promptTemplateVersion: number;
|
|
17
|
-
}
|
|
1
|
+
import { PerformAiEditParams, PerformAiEditResult } from "./types";
|
|
18
2
|
/**
|
|
19
3
|
* Core AI logic for:
|
|
20
4
|
* - Building the prompt (including context)
|
|
21
|
-
* - Calling the LLM
|
|
5
|
+
* - Calling the provided LLM function
|
|
22
6
|
* - Parsing and merging file changes
|
|
23
7
|
* - Retrieving cost metadata
|
|
24
8
|
*/
|
|
25
|
-
export declare function performAiEdit({ prompt,
|
|
9
|
+
export declare function performAiEdit({ prompt, files, llmFunction, apiKey, }: PerformAiEditParams): Promise<PerformAiEditResult>;
|
package/dist/index.js
CHANGED
|
@@ -1,161 +1,40 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.performAiEdit = performAiEdit;
|
|
4
|
-
const openai_1 = require("@langchain/openai");
|
|
5
|
-
const output_parsers_1 = require("@langchain/core/output_parsers");
|
|
6
4
|
const llm_code_format_1 = require("llm-code-format");
|
|
7
|
-
const
|
|
5
|
+
const prompt_1 = require("./prompt");
|
|
6
|
+
const metadata_1 = require("./metadata");
|
|
7
|
+
const fileUtils_1 = require("./fileUtils");
|
|
8
8
|
const debug = false;
|
|
9
|
-
// Versions of the prompt template.
|
|
10
|
-
const PROMPT_TEMPLATE_VERSION = 1;
|
|
11
|
-
// Template pieces
|
|
12
|
-
const TASK = (prompt) => `## Your Task\n\n${prompt}`;
|
|
13
|
-
const FILES = (filesContext) => `## Original Files\n\n${filesContext}`;
|
|
14
|
-
const FORMAT = [
|
|
15
|
-
"## Formatting Instructions\n\n",
|
|
16
|
-
"Suggest changes to the original files using this exact format:\n\n",
|
|
17
|
-
"**fileA.js**\n\n```js\n// Entire updated code for fileA\n```\n\n",
|
|
18
|
-
"**fileB.js**\n\n```js\n// Entire updated code for fileB\n```\n\n",
|
|
19
|
-
"Only include the files that need to be updated or created.\n\n",
|
|
20
|
-
"To suggest changes you MUST include the ENTIRE content of the updated file.\n\n",
|
|
21
|
-
'NEVER leave out sections as in "... rest of the code remain the same ...".\n\n',
|
|
22
|
-
"Refactor large files into smaller files in the same directory.\n\n",
|
|
23
|
-
"Delete all unused files, but we need to keep `README.md`. ",
|
|
24
|
-
"Files can be deleted by setting their content to empty, for example:\n\n",
|
|
25
|
-
"**fileToDelete.js**\n\n```\n```\n\n",
|
|
26
|
-
"For D3 logic, make sure it remains idempotent (use data joins), ",
|
|
27
|
-
"and prefer function signatures like `someFunction(selection, options)` ",
|
|
28
|
-
"where `selection` is a D3 selection and `options` is an object.\n\n",
|
|
29
|
-
].join("");
|
|
30
|
-
function assembleFullPrompt({ filesContext, prompt, }) {
|
|
31
|
-
return [TASK(prompt), FILES(filesContext), FORMAT].join("\n\n");
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* If the LLM outputs empty text for a file, we interpret this
|
|
35
|
-
* as a request to delete the file.
|
|
36
|
-
*/
|
|
37
|
-
function shouldDeleteFile(file) {
|
|
38
|
-
return file && file.text.trim() === "";
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Get cost and metadata for a generation from OpenRouter.
|
|
42
|
-
*/
|
|
43
|
-
async function getGenerationMetadata({ apiKey, generationId, }) {
|
|
44
|
-
const url = `https://openrouter.ai/api/v1/generation?id=${generationId}`;
|
|
45
|
-
const headers = {
|
|
46
|
-
Authorization: `Bearer ${apiKey}`,
|
|
47
|
-
};
|
|
48
|
-
// Retry logic for potential 404 from OpenRouter
|
|
49
|
-
const maxRetries = 10;
|
|
50
|
-
const retryDelay = 1000; // 1 second
|
|
51
|
-
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
52
|
-
const response = await fetch(url, {
|
|
53
|
-
method: "GET",
|
|
54
|
-
headers,
|
|
55
|
-
});
|
|
56
|
-
if (response.ok) {
|
|
57
|
-
const data = await response.json();
|
|
58
|
-
debug && console.log("[getCost] data:", JSON.stringify(data, null, 2));
|
|
59
|
-
const upstreamCostInDollars = data.data.total_cost;
|
|
60
|
-
const upstreamCostCents = upstreamCostInDollars * 100;
|
|
61
|
-
const provider = data.data.provider_name;
|
|
62
|
-
const inputTokens = data.data.tokens_prompt;
|
|
63
|
-
const outputTokens = data.data.tokens_completion;
|
|
64
|
-
return {
|
|
65
|
-
upstreamCostCents,
|
|
66
|
-
provider,
|
|
67
|
-
inputTokens,
|
|
68
|
-
outputTokens,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
const text = await response.text();
|
|
73
|
-
debug &&
|
|
74
|
-
console.error(`Attempt ${attempt} failed. Status: ${response.status}, response text: ${text}`);
|
|
75
|
-
if (attempt < maxRetries) {
|
|
76
|
-
debug && console.log(`Retrying in ${retryDelay / 1000} seconds...`);
|
|
77
|
-
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
78
|
-
}
|
|
79
|
-
else {
|
|
80
|
-
throw new Error(`HTTP error! Status: ${response.status} after ${maxRetries} attempts.`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
// You theoretically never reach here because of the throw above
|
|
85
|
-
throw new Error("Failed to get generation metadata.");
|
|
86
|
-
}
|
|
87
9
|
/**
|
|
88
10
|
* Core AI logic for:
|
|
89
11
|
* - Building the prompt (including context)
|
|
90
|
-
* - Calling the LLM
|
|
12
|
+
* - Calling the provided LLM function
|
|
91
13
|
* - Parsing and merging file changes
|
|
92
14
|
* - Retrieving cost metadata
|
|
93
15
|
*/
|
|
94
|
-
async function performAiEdit({ prompt,
|
|
95
|
-
// 1.
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
// Example: truncate large files, etc.
|
|
99
|
-
text: file.text
|
|
100
|
-
.split("\n")
|
|
101
|
-
.slice(0, file.name.endsWith(".csv") || file.name.endsWith(".json") ? 50 : 500)
|
|
102
|
-
.map((line) => line.slice(0, 200))
|
|
103
|
-
.join("\n"),
|
|
104
|
-
})));
|
|
16
|
+
async function performAiEdit({ prompt, files, llmFunction, apiKey, }) {
|
|
17
|
+
// 1. Format the existing files into the "markdown code block" format
|
|
18
|
+
const preparedFiles = (0, fileUtils_1.prepareFilesForPrompt)(files);
|
|
19
|
+
const filesContext = (0, llm_code_format_1.formatMarkdownFiles)(preparedFiles);
|
|
105
20
|
// 2. Assemble the final prompt
|
|
106
|
-
const fullPrompt = assembleFullPrompt({ filesContext, prompt });
|
|
21
|
+
const fullPrompt = (0, prompt_1.assembleFullPrompt)({ filesContext, prompt });
|
|
107
22
|
debug && console.log("[performAiEdit] fullPrompt:", fullPrompt);
|
|
108
|
-
// 3. Invoke the model via
|
|
109
|
-
const
|
|
110
|
-
modelName,
|
|
111
|
-
configuration: {
|
|
112
|
-
apiKey,
|
|
113
|
-
baseURL,
|
|
114
|
-
},
|
|
115
|
-
streaming: false,
|
|
116
|
-
};
|
|
117
|
-
const chatModel = new openai_1.ChatOpenAI(options);
|
|
118
|
-
const result = await chatModel.invoke(fullPrompt);
|
|
23
|
+
// 3. Invoke the model via the provided LLM function
|
|
24
|
+
const result = await llmFunction(fullPrompt);
|
|
119
25
|
// 4. We parse the output to figure out which files changed
|
|
120
|
-
const
|
|
121
|
-
const resultString = await parser.invoke(result);
|
|
26
|
+
const resultString = result.content;
|
|
122
27
|
const parsed = (0, llm_code_format_1.parseMarkdownFiles)(resultString, "bold");
|
|
123
28
|
// 5. Merge the changes into a new `Files` object
|
|
124
|
-
|
|
125
|
-
const original = files[fileId];
|
|
126
|
-
const changedFile = parsed.files.find((f) => f.name === original.name);
|
|
127
|
-
if (shouldDeleteFile(changedFile)) {
|
|
128
|
-
// Exclude from new set
|
|
129
|
-
return acc;
|
|
130
|
-
}
|
|
131
|
-
// If changedFile is present, use it; otherwise use original
|
|
132
|
-
acc[fileId] = {
|
|
133
|
-
...original,
|
|
134
|
-
text: changedFile ? changedFile.text : original.text,
|
|
135
|
-
};
|
|
136
|
-
return acc;
|
|
137
|
-
}, {});
|
|
138
|
-
// Handle newly-created files
|
|
139
|
-
parsed.files.forEach((changedFile) => {
|
|
140
|
-
const existingFile = Object.values(changedFiles).find((file) => file.name === changedFile.name);
|
|
141
|
-
// If no existing file and not empty => it's a new file
|
|
142
|
-
if (!existingFile && !shouldDeleteFile(changedFile)) {
|
|
143
|
-
const newFileId = (0, viz_types_1.generateVizFileId)();
|
|
144
|
-
changedFiles[newFileId] = {
|
|
145
|
-
name: changedFile.name,
|
|
146
|
-
text: changedFile.text,
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
});
|
|
29
|
+
const changedFiles = (0, fileUtils_1.mergeFileChanges)(files, parsed.files);
|
|
150
30
|
// 6. Retrieve cost metadata for charging the user
|
|
151
|
-
|
|
152
|
-
const openRouterGenerationId = result.lc_kwargs?.id;
|
|
31
|
+
const openRouterGenerationId = result.generationId || "";
|
|
153
32
|
let upstreamCostCents = 0;
|
|
154
33
|
let provider = "";
|
|
155
34
|
let inputTokens = 0;
|
|
156
35
|
let outputTokens = 0;
|
|
157
|
-
if (openRouterGenerationId) {
|
|
158
|
-
const costData = await getGenerationMetadata({
|
|
36
|
+
if (openRouterGenerationId && apiKey) {
|
|
37
|
+
const costData = await (0, metadata_1.getGenerationMetadata)({
|
|
159
38
|
apiKey,
|
|
160
39
|
generationId: openRouterGenerationId,
|
|
161
40
|
});
|
|
@@ -166,11 +45,12 @@ async function performAiEdit({ prompt, modelName, files, apiKey, baseURL, }) {
|
|
|
166
45
|
}
|
|
167
46
|
return {
|
|
168
47
|
changedFiles,
|
|
169
|
-
openRouterGenerationId
|
|
48
|
+
openRouterGenerationId,
|
|
170
49
|
upstreamCostCents,
|
|
171
50
|
provider,
|
|
172
51
|
inputTokens,
|
|
173
52
|
outputTokens,
|
|
174
|
-
promptTemplateVersion: PROMPT_TEMPLATE_VERSION,
|
|
53
|
+
promptTemplateVersion: prompt_1.PROMPT_TEMPLATE_VERSION,
|
|
54
|
+
rawResponse: resultString, // Include the raw response
|
|
175
55
|
};
|
|
176
56
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get cost and metadata for a generation from OpenRouter.
|
|
3
|
+
*/
|
|
4
|
+
export declare function getGenerationMetadata({ apiKey, generationId, }: {
|
|
5
|
+
apiKey: string;
|
|
6
|
+
generationId: string;
|
|
7
|
+
}): Promise<{
|
|
8
|
+
upstreamCostCents: number;
|
|
9
|
+
provider: string;
|
|
10
|
+
inputTokens: number;
|
|
11
|
+
outputTokens: number;
|
|
12
|
+
}>;
|
package/dist/metadata.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getGenerationMetadata = getGenerationMetadata;
|
|
4
|
+
const debug = false;
|
|
5
|
+
/**
|
|
6
|
+
* Get cost and metadata for a generation from OpenRouter.
|
|
7
|
+
*/
|
|
8
|
+
async function getGenerationMetadata({ apiKey, generationId, }) {
|
|
9
|
+
const url = `https://openrouter.ai/api/v1/generation?id=${generationId}`;
|
|
10
|
+
const headers = {
|
|
11
|
+
Authorization: `Bearer ${apiKey}`,
|
|
12
|
+
};
|
|
13
|
+
// Retry logic for potential 404 from OpenRouter
|
|
14
|
+
const maxRetries = 10;
|
|
15
|
+
const retryDelay = 1000; // 1 second
|
|
16
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
17
|
+
const response = await fetch(url, {
|
|
18
|
+
method: "GET",
|
|
19
|
+
headers,
|
|
20
|
+
});
|
|
21
|
+
if (response.ok) {
|
|
22
|
+
const data = await response.json();
|
|
23
|
+
debug && console.log("[getCost] data:", JSON.stringify(data, null, 2));
|
|
24
|
+
const upstreamCostInDollars = data.data.total_cost;
|
|
25
|
+
const upstreamCostCents = upstreamCostInDollars * 100;
|
|
26
|
+
const provider = data.data.provider_name;
|
|
27
|
+
const inputTokens = data.data.tokens_prompt;
|
|
28
|
+
const outputTokens = data.data.tokens_completion;
|
|
29
|
+
return {
|
|
30
|
+
upstreamCostCents,
|
|
31
|
+
provider,
|
|
32
|
+
inputTokens,
|
|
33
|
+
outputTokens,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const text = await response.text();
|
|
38
|
+
debug &&
|
|
39
|
+
console.error(`Attempt ${attempt} failed. Status: ${response.status}, response text: ${text}`);
|
|
40
|
+
if (attempt < maxRetries) {
|
|
41
|
+
debug && console.log(`Retrying in ${retryDelay / 1000} seconds...`);
|
|
42
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
throw new Error(`HTTP error! Status: ${response.status} after ${maxRetries} attempts.`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// You theoretically never reach here because of the throw above
|
|
50
|
+
throw new Error("Failed to get generation metadata.");
|
|
51
|
+
}
|
package/dist/prompt.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const PROMPT_TEMPLATE_VERSION = 1;
|
|
2
|
+
/**
|
|
3
|
+
* Assembles the full prompt by combining task, files context, and formatting instructions
|
|
4
|
+
*/
|
|
5
|
+
export declare function assembleFullPrompt({ filesContext, prompt, }: {
|
|
6
|
+
filesContext: string;
|
|
7
|
+
prompt: string;
|
|
8
|
+
}): string;
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PROMPT_TEMPLATE_VERSION = void 0;
|
|
4
|
+
exports.assembleFullPrompt = assembleFullPrompt;
|
|
5
|
+
// Versions of the prompt template.
|
|
6
|
+
exports.PROMPT_TEMPLATE_VERSION = 1;
|
|
7
|
+
// Template pieces
|
|
8
|
+
const TASK = (prompt) => `## Your Task\n\n${prompt}`;
|
|
9
|
+
const FILES = (filesContext) => `## Original Files\n\n${filesContext}`;
|
|
10
|
+
const FORMAT = [
|
|
11
|
+
"## Formatting Instructions\n\n",
|
|
12
|
+
"Suggest changes to the original files using this exact format:\n\n",
|
|
13
|
+
"**fileA.js**\n\n```js\n// Entire updated code for fileA\n```\n\n",
|
|
14
|
+
"**fileB.js**\n\n```js\n// Entire updated code for fileB\n```\n\n",
|
|
15
|
+
"Only include the files that need to be updated or created.\n\n",
|
|
16
|
+
"To suggest changes you MUST include the ENTIRE content of the updated file.\n\n",
|
|
17
|
+
'NEVER leave out sections as in "... rest of the code remain the same ...".\n\n',
|
|
18
|
+
"Refactor large files into smaller files in the same directory.\n\n",
|
|
19
|
+
"Delete all unused files, but we need to keep `README.md`. ",
|
|
20
|
+
"Files can be deleted by setting their content to empty, for example:\n\n",
|
|
21
|
+
"**fileToDelete.js**\n\n```\n```\n\n",
|
|
22
|
+
"For D3 logic, make sure it remains idempotent (use data joins), ",
|
|
23
|
+
"and prefer function signatures like `someFunction(selection, options)` ",
|
|
24
|
+
"where `selection` is a D3 selection and `options` is an object.\n\n",
|
|
25
|
+
].join("");
|
|
26
|
+
/**
|
|
27
|
+
* Assembles the full prompt by combining task, files context, and formatting instructions
|
|
28
|
+
*/
|
|
29
|
+
function assembleFullPrompt({ filesContext, prompt, }) {
|
|
30
|
+
return [TASK(prompt), FILES(filesContext), FORMAT].join("\n\n");
|
|
31
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { VizFiles } from "@vizhub/viz-types";
|
|
2
|
+
export type LlmFunction = (prompt: string) => Promise<{
|
|
3
|
+
content: string;
|
|
4
|
+
generationId?: string;
|
|
5
|
+
}>;
|
|
6
|
+
export interface PerformAiEditParams {
|
|
7
|
+
prompt: string;
|
|
8
|
+
files: VizFiles;
|
|
9
|
+
llmFunction: LlmFunction;
|
|
10
|
+
apiKey?: string;
|
|
11
|
+
baseURL?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface PerformAiEditResult {
|
|
14
|
+
changedFiles: VizFiles;
|
|
15
|
+
openRouterGenerationId?: string;
|
|
16
|
+
upstreamCostCents?: number;
|
|
17
|
+
provider?: string;
|
|
18
|
+
inputTokens?: number;
|
|
19
|
+
outputTokens?: number;
|
|
20
|
+
promptTemplateVersion?: number;
|
|
21
|
+
rawResponse?: string;
|
|
22
|
+
}
|
package/dist/types.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "editcodewithai",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Edit Code With AI",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
"test": "vitest run",
|
|
13
13
|
"build": "tsc",
|
|
14
14
|
"prepublishOnly": "npm run build",
|
|
15
|
-
"
|
|
15
|
+
"typecheck": "tsc --noEmit",
|
|
16
|
+
"benchmark": "ts-node src/benchmarks/cli.ts run; cp -r ./benchmarks grader-app/public",
|
|
17
|
+
"grade": "ts-node src/benchmarks/cli.ts grade",
|
|
18
|
+
"benchmark:help": "ts-node src/benchmarks/cli.ts help",
|
|
19
|
+
"upgrade": "ncu -u",
|
|
20
|
+
"prettier": "prettier {*.*,**/*.*} --write"
|
|
16
21
|
},
|
|
17
22
|
"repository": {
|
|
18
23
|
"type": "git",
|
|
@@ -35,13 +40,24 @@
|
|
|
35
40
|
},
|
|
36
41
|
"homepage": "https://github.com/vizhub-core/editcodewithai#readme",
|
|
37
42
|
"dependencies": {
|
|
38
|
-
"@langchain/core": "^0.3.
|
|
39
|
-
"@langchain/openai": "^0.4
|
|
40
|
-
"@
|
|
41
|
-
"
|
|
43
|
+
"@langchain/core": "^0.3.43",
|
|
44
|
+
"@langchain/openai": "^0.5.4",
|
|
45
|
+
"@types/d3": "^7.4.3",
|
|
46
|
+
"@vizhub/viz-types": "^0.1.0",
|
|
47
|
+
"@vizhub/viz-utils": "^0.1.0",
|
|
48
|
+
"d3": "^7.9.0",
|
|
49
|
+
"dotenv": "^16.4.7",
|
|
50
|
+
"langchain": "^0.3.20",
|
|
51
|
+
"llm-code-format": "^2.0.1"
|
|
42
52
|
},
|
|
43
53
|
"devDependencies": {
|
|
44
|
-
"
|
|
45
|
-
"
|
|
54
|
+
"cors": "^2.8.5",
|
|
55
|
+
"express": "^5.1.0",
|
|
56
|
+
"npm-check-updates": "^17.1.16",
|
|
57
|
+
"prettier": "^3.5.3",
|
|
58
|
+
"puppeteer": "^24.6.0",
|
|
59
|
+
"ts-node": "^10.9.2",
|
|
60
|
+
"typescript": "^5.8.3",
|
|
61
|
+
"vitest": "^3.1.1"
|
|
46
62
|
}
|
|
47
63
|
}
|