editcodewithai 1.0.0 → 2.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 CHANGED
@@ -2,12 +2,16 @@
2
2
 
3
3
  A lightweight, flexible library for AI-powered code editing.
4
4
 
5
+ See also [vizhub-benchmarks](https://github.com/vizhub-core/vizhub-benchmarks).
6
+
5
7
  ## Overview
6
8
 
7
9
  `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.
8
10
 
9
11
  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.
10
12
 
13
+ Edit formats inspired by [Aider](https://aider.chat/). See [Aider: Edit Formats](https://aider.chat/docs/more/edit-formats.html) for details.
14
+
11
15
  ## Installation
12
16
 
13
17
  ```bash
@@ -16,93 +20,79 @@ npm install editcodewithai
16
20
 
17
21
  ## Usage
18
22
 
23
+ Here's a basic example of how to use `performAiEdit` to update a file:
24
+
19
25
  ```typescript
20
- import { performAiEdit } from "editcodewithai";
26
+ import { performAiEdit, LlmFunction } from "editcodewithai";
21
27
  import { VizFiles } from "@vizhub/viz-types";
22
28
 
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();
29
+ // Your function to call the LLM
30
+ const myLlmFunction: LlmFunction = async (prompt: string) => {
31
+ // ... call your LLM API with the prompt
32
+ const llmResponse = "...";
43
33
  return {
44
- content: data.choices[0].message.content,
45
- generationId: data.id,
34
+ content: llmResponse, // The raw string response from the LLM
35
+ generationId: "some-generation-id", // Optional, for cost tracking with OpenRouter
46
36
  };
47
37
  };
48
38
 
49
- // Your files
50
39
  const files: VizFiles = {
51
40
  file1: {
52
41
  name: "index.js",
53
- text: "console.log('Hello world');",
42
+ text: 'console.log("Hello, World!");',
54
43
  },
55
44
  };
56
45
 
57
- // Perform the AI edit
58
- const result = await performAiEdit({
59
- prompt: "Update the code to use async/await",
60
- files: files,
61
- llmFunction: myLlmFunction,
62
- apiKey: "your-openrouter-api-key",
63
- });
46
+ const prompt = 'Change the greeting to "Hello, Universe!"';
64
47
 
65
- console.log(result.changedFiles);
66
- ```
48
+ async function main() {
49
+ const result = await performAiEdit({
50
+ prompt,
51
+ files,
52
+ llmFunction: myLlmFunction,
53
+ editFormat: "diff", // Specify the desired edit format
54
+ });
67
55
 
68
- ## API Reference
56
+ console.log(result.changedFiles["file1"].text);
57
+ // Expected output: console.log("Hello, Universe!");
58
+ }
69
59
 
70
- ### performAiEdit(params)
60
+ main();
61
+ ```
71
62
 
72
- The main function that processes files with an AI model and returns edited code.
63
+ ## Edit Formats
73
64
 
74
- #### Parameters
65
+ This library supports several "edit formats" that instruct the LLM on how to specify file changes. Different models may perform better with different formats. You can specify the format using the `editFormat` parameter in `performAiEdit`.
75
66
 
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 |
67
+ ### `whole` (default)
82
68
 
83
- The `VizFiles` type is a map of file IDs to file objects, where each file object has:
69
+ The LLM returns the complete, updated content for each file that needs changes. This is simple but can be inefficient for large files with small changes.
84
70
 
85
- - `name`: The filename (e.g., "index.js")
86
- - `text`: The file contents as a string
71
+ **Example:**
87
72
 
88
- The `LlmFunction` type is a function that takes a prompt string and returns a Promise with:
73
+ ````
74
+ index.js
75
+ ```js
76
+ console.log("Hello, Universe!");
77
+ ```
78
+ ````
89
79
 
90
- - `content`: The LLM's response text
91
- - `generationId`: A unique ID for the generation (used for cost tracking)
80
+ ### `diff`
92
81
 
93
- #### Return Value
82
+ The LLM returns a series of search-and-replace blocks. This is efficient as it only includes the changed portions of the files.
94
83
 
95
- The function returns an object with:
84
+ **Example:**
96
85
 
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 |
86
+ ````
87
+ index.js
88
+ ```
89
+ <<<<<<< SEARCH
90
+ console.log("Hello, World!");
91
+ =======
92
+ console.log("Hello, Universe!");
93
+ >>>>>>> REPLACE
94
+ ```
95
+ ````
106
96
 
107
97
  ### File Operations
108
98
 
@@ -12,3 +12,18 @@ export declare function prepareFilesForPrompt(files: VizFiles): FileCollection;
12
12
  * Merges original files with changes from the LLM
13
13
  */
14
14
  export declare function mergeFileChanges(originalFiles: VizFiles, parsedFiles: FileCollection): VizFiles;
15
+ export interface Diff {
16
+ fileName: string;
17
+ search: string;
18
+ replace: string;
19
+ }
20
+ export declare function parseDiffs(responseText: string): Diff[];
21
+ export declare function applyDiffs(originalFiles: VizFiles, diffs: Diff[]): VizFiles;
22
+ export declare function parseDiffFenced(responseText: string): Diff[];
23
+ export interface UdiffHunk {
24
+ fileName: string;
25
+ original: string;
26
+ updated: string;
27
+ }
28
+ export declare function parseUdiffs(responseText: string): UdiffHunk[];
29
+ export declare function applyUdiffs(originalFiles: VizFiles, hunks: UdiffHunk[]): VizFiles;
package/dist/fileUtils.js CHANGED
@@ -3,6 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.shouldDeleteFile = shouldDeleteFile;
4
4
  exports.prepareFilesForPrompt = prepareFilesForPrompt;
5
5
  exports.mergeFileChanges = mergeFileChanges;
6
+ exports.parseDiffs = parseDiffs;
7
+ exports.applyDiffs = applyDiffs;
8
+ exports.parseDiffFenced = parseDiffFenced;
9
+ exports.parseUdiffs = parseUdiffs;
10
+ exports.applyUdiffs = applyUdiffs;
6
11
  const viz_utils_1 = require("@vizhub/viz-utils");
7
12
  /**
8
13
  * If the LLM outputs empty text for a file, we interpret this
@@ -64,3 +69,100 @@ function mergeFileChanges(originalFiles, parsedFiles) {
64
69
  });
65
70
  return changedFiles;
66
71
  }
72
+ function parseDiffs(responseText) {
73
+ const diffs = [];
74
+ // This regex captures the file path, and the content of the SEARCH and REPLACE blocks.
75
+ const diffRegex = /^(.+)\n```\n<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> REPLACE\n```/gm;
76
+ const matches = responseText.matchAll(diffRegex);
77
+ for (const match of matches) {
78
+ const [_, fileName, search, replace] = match;
79
+ diffs.push({
80
+ fileName: fileName.trim(),
81
+ search,
82
+ replace,
83
+ });
84
+ }
85
+ return diffs;
86
+ }
87
+ function applyDiffs(originalFiles, diffs) {
88
+ // Create a mutable copy of the files to avoid side effects.
89
+ const changedFiles = JSON.parse(JSON.stringify(originalFiles));
90
+ for (const diff of diffs) {
91
+ const fileId = Object.keys(changedFiles).find((id) => changedFiles[id].name === diff.fileName);
92
+ if (!fileId) {
93
+ throw new Error(`File not found: ${diff.fileName}`);
94
+ }
95
+ const file = changedFiles[fileId];
96
+ if (!file.text.includes(diff.search)) {
97
+ throw new Error(`Search block not found in file: ${diff.fileName}`);
98
+ }
99
+ // Replace only the first occurrence, which is the standard behavior of string.replace.
100
+ file.text = file.text.replace(diff.search, diff.replace);
101
+ }
102
+ return changedFiles;
103
+ }
104
+ function parseDiffFenced(responseText) {
105
+ const diffs = [];
106
+ const diffRegex = /^```\n(.+)\n<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> REPLACE\n```/gm;
107
+ const matches = responseText.matchAll(diffRegex);
108
+ for (const match of matches) {
109
+ const [_, fileName, search, replace] = match;
110
+ diffs.push({
111
+ fileName: fileName.trim(),
112
+ search,
113
+ replace,
114
+ });
115
+ }
116
+ return diffs;
117
+ }
118
+ function parseUdiffs(responseText) {
119
+ const hunks = [];
120
+ const udiffFileRegex = /```diff\n--- (.+?)\n\+\+\+ \1\n([\s\S]+?)```/g;
121
+ let fileMatch;
122
+ while ((fileMatch = udiffFileRegex.exec(responseText)) !== null) {
123
+ const fileName = fileMatch[1].trim();
124
+ const allHunksContent = fileMatch[2];
125
+ const hunkParts = allHunksContent.split(/^@@ .* @@$/m).slice(1);
126
+ for (const part of hunkParts) {
127
+ if (part.trim() === "")
128
+ continue;
129
+ const lines = part.trim().split("\n");
130
+ const original = [];
131
+ const updated = [];
132
+ for (const line of lines) {
133
+ if (line.startsWith("+")) {
134
+ updated.push(line.substring(1));
135
+ }
136
+ else if (line.startsWith("-")) {
137
+ original.push(line.substring(1));
138
+ }
139
+ else {
140
+ const content = line.startsWith(" ") ? line.substring(1) : line;
141
+ original.push(content);
142
+ updated.push(content);
143
+ }
144
+ }
145
+ hunks.push({
146
+ fileName: fileName,
147
+ original: original.join("\n"),
148
+ updated: updated.join("\n"),
149
+ });
150
+ }
151
+ }
152
+ return hunks;
153
+ }
154
+ function applyUdiffs(originalFiles, hunks) {
155
+ const changedFiles = JSON.parse(JSON.stringify(originalFiles));
156
+ for (const hunk of hunks) {
157
+ const fileId = Object.keys(changedFiles).find((id) => changedFiles[id].name === hunk.fileName);
158
+ if (!fileId) {
159
+ throw new Error(`File not found: ${hunk.fileName}`);
160
+ }
161
+ const file = changedFiles[fileId];
162
+ if (!file.text.includes(hunk.original)) {
163
+ throw new Error(`Original content for hunk not found in file: ${hunk.fileName}`);
164
+ }
165
+ file.text = file.text.replace(hunk.original, hunk.updated);
166
+ }
167
+ return changedFiles;
168
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { PerformAiEditParams, PerformAiEditResult } from "./types";
1
+ import type { PerformAiEditParams, PerformAiEditResult } from "./types";
2
+ export type { LlmFunction, PerformAiEditParams, PerformAiEditResult, EditFormat, } from "./types";
2
3
  /**
3
4
  * Core AI logic for:
4
5
  * - Building the prompt (including context)
@@ -6,4 +7,4 @@ import { PerformAiEditParams, PerformAiEditResult } from "./types";
6
7
  * - Parsing and merging file changes
7
8
  * - Retrieving cost metadata
8
9
  */
9
- export declare function performAiEdit({ prompt, files, llmFunction, apiKey, }: PerformAiEditParams): Promise<PerformAiEditResult>;
10
+ export declare function performAiEdit({ prompt, files, llmFunction, apiKey, editFormat, }: PerformAiEditParams): Promise<PerformAiEditResult>;
package/dist/index.js CHANGED
@@ -13,20 +13,43 @@ const debug = false;
13
13
  * - Parsing and merging file changes
14
14
  * - Retrieving cost metadata
15
15
  */
16
- async function performAiEdit({ prompt, files, llmFunction, apiKey, }) {
16
+ async function performAiEdit({ prompt, files, llmFunction, apiKey, editFormat = "whole", }) {
17
17
  // 1. Format the existing files into the "markdown code block" format
18
18
  const preparedFiles = (0, fileUtils_1.prepareFilesForPrompt)(files);
19
19
  const filesContext = (0, llm_code_format_1.formatMarkdownFiles)(preparedFiles);
20
20
  // 2. Assemble the final prompt
21
- const fullPrompt = (0, prompt_1.assembleFullPrompt)({ filesContext, prompt });
21
+ const fullPrompt = (0, prompt_1.assembleFullPrompt)({ filesContext, prompt, editFormat });
22
22
  debug && console.log("[performAiEdit] fullPrompt:", fullPrompt);
23
23
  // 3. Invoke the model via the provided LLM function
24
24
  const result = await llmFunction(fullPrompt);
25
25
  // 4. We parse the output to figure out which files changed
26
26
  const resultString = result.content;
27
- const parsed = (0, llm_code_format_1.parseMarkdownFiles)(resultString, "bold");
28
- // 5. Merge the changes into a new `Files` object
29
- const changedFiles = (0, fileUtils_1.mergeFileChanges)(files, parsed.files);
27
+ let changedFiles;
28
+ switch (editFormat) {
29
+ case "whole": {
30
+ const parsed = (0, llm_code_format_1.parseMarkdownFiles)(resultString, "bold");
31
+ changedFiles = (0, fileUtils_1.mergeFileChanges)(files, parsed.files);
32
+ break;
33
+ }
34
+ case "diff": {
35
+ const diffs = (0, fileUtils_1.parseDiffs)(resultString);
36
+ changedFiles = (0, fileUtils_1.applyDiffs)(files, diffs);
37
+ break;
38
+ }
39
+ case "diff-fenced": {
40
+ const diffs = (0, fileUtils_1.parseDiffFenced)(resultString);
41
+ changedFiles = (0, fileUtils_1.applyDiffs)(files, diffs);
42
+ break;
43
+ }
44
+ case "udiff": {
45
+ const hunks = (0, fileUtils_1.parseUdiffs)(resultString);
46
+ changedFiles = (0, fileUtils_1.applyUdiffs)(files, hunks);
47
+ break;
48
+ }
49
+ default:
50
+ // This will catch any unhandled or unknown edit formats.
51
+ throw new Error(`Unknown edit format: ${editFormat}`);
52
+ }
30
53
  // 6. Retrieve cost metadata for charging the user
31
54
  const openRouterGenerationId = result.generationId || "";
32
55
  let upstreamCostCents = 0;
package/dist/prompt.d.ts CHANGED
@@ -1,8 +1,10 @@
1
+ import type { EditFormat } from "./types";
1
2
  export declare const PROMPT_TEMPLATE_VERSION = 1;
2
3
  /**
3
4
  * Assembles the full prompt by combining task, files context, and formatting instructions
4
5
  */
5
- export declare function assembleFullPrompt({ filesContext, prompt, }: {
6
+ export declare function assembleFullPrompt({ filesContext, prompt, editFormat, }: {
6
7
  filesContext: string;
7
8
  prompt: string;
9
+ editFormat?: EditFormat;
8
10
  }): string;
package/dist/prompt.js CHANGED
@@ -7,25 +7,63 @@ exports.PROMPT_TEMPLATE_VERSION = 1;
7
7
  // Template pieces
8
8
  const TASK = (prompt) => `## Your Task\n\n${prompt}`;
9
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("");
10
+ const FORMAT_INSTRUCTIONS = {
11
+ whole: [
12
+ "## Formatting Instructions\n\n",
13
+ "Suggest changes to the original files using this exact format:\n\n",
14
+ "**fileA.js**\n\n```js\n// Entire updated code for fileA\n```\n\n",
15
+ "**fileB.js**\n\n```js\n// Entire updated code for fileB\n```\n\n",
16
+ "Only include the files that need to be updated or created.\n\n",
17
+ "To suggest changes you MUST include the ENTIRE content of the updated file.\n\n",
18
+ 'NEVER leave out sections as in "... rest of the code remain the same ...".\n\n',
19
+ "Refactor large files into smaller files in the same directory.\n\n",
20
+ "Delete all unused files, but we need to keep `README.md`. ",
21
+ "Files can be deleted by setting their content to empty, for example:\n\n",
22
+ "**fileToDelete.js**\n\n```\n```\n\n",
23
+ "For D3 logic, make sure it remains idempotent (use data joins), ",
24
+ "and prefer function signatures like `someFunction(selection, options)` ",
25
+ "where `selection` is a D3 selection and `options` is an object.\n\n",
26
+ ].join(""),
27
+ diff: [
28
+ "## Formatting Instructions\n\n",
29
+ "Suggest changes to the original files using this search/replace block format:\n\n",
30
+ "path/to/filename.ext\n",
31
+ "```\n",
32
+ "<<<<<<< SEARCH\n",
33
+ "// code to be replaced\n",
34
+ "=======\n",
35
+ "// new code\n",
36
+ ">>>>>>> REPLACE\n",
37
+ "```\n",
38
+ ].join(""),
39
+ "diff-fenced": [
40
+ "## Formatting Instructions\n\n",
41
+ "Suggest changes to the original files using this search/replace block format with the file path inside the fence:\n\n",
42
+ "```\n",
43
+ "path/to/filename.ext\n",
44
+ "<<<<<<< SEARCH\n",
45
+ "// code to be replaced\n",
46
+ "=======\n",
47
+ "// new code\n",
48
+ ">>>>>>> REPLACE\n",
49
+ "```\n",
50
+ ].join(""),
51
+ udiff: [
52
+ "## Formatting Instructions\n\n",
53
+ "Suggest changes to the original files using the unified diff format:\n\n",
54
+ "```diff\n",
55
+ "--- path/to/filename.ext\n",
56
+ "+++ path/to/filename.ext\n",
57
+ "@@ ... @@\n",
58
+ "-// line to be removed\n",
59
+ "+// line to be added\n",
60
+ "```\n",
61
+ ].join(""),
62
+ };
26
63
  /**
27
64
  * Assembles the full prompt by combining task, files context, and formatting instructions
28
65
  */
29
- function assembleFullPrompt({ filesContext, prompt, }) {
66
+ function assembleFullPrompt({ filesContext, prompt, editFormat = "whole", }) {
67
+ const FORMAT = FORMAT_INSTRUCTIONS[editFormat];
30
68
  return [TASK(prompt), FILES(filesContext), FORMAT].join("\n\n");
31
69
  }
package/dist/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { VizFiles } from "@vizhub/viz-types";
2
+ export type EditFormat = "whole" | "diff" | "diff-fenced" | "udiff";
2
3
  export type LlmFunction = (prompt: string) => Promise<{
3
4
  content: string;
4
5
  generationId?: string;
@@ -8,7 +9,7 @@ export interface PerformAiEditParams {
8
9
  files: VizFiles;
9
10
  llmFunction: LlmFunction;
10
11
  apiKey?: string;
11
- baseURL?: string;
12
+ editFormat?: EditFormat;
12
13
  }
13
14
  export interface PerformAiEditResult {
14
15
  changedFiles: VizFiles;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "editcodewithai",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Edit Code With AI",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -13,7 +13,7 @@
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",
16
+ "benchmark": "vite-node src/benchmarks/cli.ts run; cp -r ./benchmarks grader-app/public",
17
17
  "grade": "ts-node src/benchmarks/cli.ts grade",
18
18
  "benchmark:help": "ts-node src/benchmarks/cli.ts help",
19
19
  "upgrade": "ncu -u",
@@ -40,24 +40,22 @@
40
40
  },
41
41
  "homepage": "https://github.com/vizhub-core/editcodewithai#readme",
42
42
  "dependencies": {
43
- "@langchain/core": "^0.3.43",
44
- "@langchain/openai": "^0.5.4",
45
- "@types/d3": "^7.4.3",
46
43
  "@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",
44
+ "@vizhub/viz-utils": "^1.1.0",
45
+ "dotenv": "^17.0.1",
51
46
  "llm-code-format": "^2.0.1"
52
47
  },
53
48
  "devDependencies": {
49
+ "@langchain/core": "^0.3.61",
50
+ "@langchain/openai": "^0.5.16",
51
+ "langchain": "^0.3.29",
54
52
  "cors": "^2.8.5",
55
53
  "express": "^5.1.0",
56
- "npm-check-updates": "^17.1.16",
57
- "prettier": "^3.5.3",
58
- "puppeteer": "^24.6.0",
54
+ "npm-check-updates": "^18.0.1",
55
+ "prettier": "^3.6.2",
56
+ "puppeteer": "^24.11.2",
59
57
  "ts-node": "^10.9.2",
60
58
  "typescript": "^5.8.3",
61
- "vitest": "^3.1.1"
59
+ "vitest": "^3.2.4"
62
60
  }
63
61
  }