opencode-fast-apply 2.1.6 → 2.2.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
@@ -1,170 +1,114 @@
1
1
  # opencode-fast-apply
2
2
 
3
- OpenCode plugin for Fast Apply - High-performance code editing with OpenAI-compatible APIs (LM Studio, Ollama).
3
+ OpenCode plugin for Fast Apply - High-performance code editing with OpenAI-compatible APIs.
4
4
 
5
5
  ## Features
6
6
 
7
- - **High-speed code editing** via OpenAI-compatible Fast Apply API (speed depends on your hardware and model)
8
- - **Lazy edit markers** (`// ... existing code ...`) - no exact string matching needed
9
- - **Unified diff output** with context for easy review
10
- - **Graceful fallback** - suggests native `edit` tool on API failure
11
- - **Multi-backend support** - LM Studio, Ollama, OpenAI, and any OpenAI-compatible endpoint
12
- - **Robust XML tag handling** - safely handles code containing `<updated-code>` tags
13
- - **Special character support** - preserves all string literals, regex patterns, and escape sequences
7
+ - **Partial file editing** - Only send relevant sections (50-500 lines), not entire files
8
+ - **Smart context matching** - Automatically finds and replaces code sections
9
+ - **Token efficient** - Save 80-98% tokens compared to full-file edits
10
+ - **Lazy edit markers** - Use `// ... existing code ...` for unchanged sections
11
+ - **Multi-backend support** - LM Studio, Ollama, OpenAI, any OpenAI-compatible endpoint
12
+ - **Robust XML handling** - Safely handles special characters and XML tags in code
14
13
 
15
14
  ## Installation
16
15
 
17
- ### 1. Install from npm (Recommended)
18
-
19
16
  ```bash
20
17
  npm install -g opencode-fast-apply
21
18
  ```
22
19
 
23
- ### 2. Configure your API endpoint
20
+ Configure your API endpoint:
24
21
 
25
- For **LM Studio** (default):
26
22
  ```bash
23
+ # LM Studio (default)
27
24
  export FAST_APPLY_URL="http://localhost:1234"
28
25
  export FAST_APPLY_MODEL="fastapply-1.5b"
29
26
  export FAST_APPLY_API_KEY="optional-api-key"
30
- ```
31
27
 
32
- For **Ollama**:
33
- ```bash
28
+ # Ollama
34
29
  export FAST_APPLY_URL="http://localhost:11434"
35
30
  export FAST_APPLY_MODEL="codellama:7b"
36
- export FAST_APPLY_API_KEY="optional-api-key"
37
- ```
38
31
 
39
- For **OpenAI**:
40
- ```bash
32
+ # OpenAI
41
33
  export FAST_APPLY_URL="https://api.openai.com"
42
34
  export FAST_APPLY_MODEL="gpt-4"
43
- export FAST_APPLY_API_KEY="sk-your-openai-key"
35
+ export FAST_APPLY_API_KEY="sk-your-key"
44
36
  ```
45
37
 
46
- **Note:** The plugin automatically handles URLs with or without `/v1` suffix.
47
-
48
- ### 3. Add the plugin to your OpenCode config
49
-
50
- Add to your global config (`~/.config/opencode/opencode.json` or `opencode.jsonc`):
38
+ Add to OpenCode config (`~/.config/opencode/opencode.json`):
51
39
 
52
40
  ```json
53
41
  {
54
- "plugin": [
55
- "opencode-fast-apply"
56
- ]
42
+ "plugin": ["opencode-fast-apply"]
57
43
  }
58
44
  ```
59
45
 
60
- **That's it!** The plugin automatically embeds all instructions - no additional configuration needed.
61
-
62
- ### 4. Restart OpenCode
63
-
64
- The `fast_apply_edit` tool will now be available and configured as the default editing tool.
46
+ Restart OpenCode. The `fast_apply_edit` tool is now available.
65
47
 
66
48
  ## Usage
67
49
 
68
- The LLM can use `fast_apply_edit` for efficient partial file edits:
50
+ ```typescript
51
+ // 1. Read relevant section (not entire file)
52
+ const content = await read("src/app.ts", { offset: 100, limit: 50 })
69
53
 
70
- ```
54
+ // 2. Edit with partial context
71
55
  fast_apply_edit({
72
- target_filepath: "sth.ts",
73
- instructions: "Add error handling for invalid tokens",
56
+ target_filepath: "src/app.ts",
57
+ original_code: content, // Just 50 lines!
74
58
  code_edit: `// ... existing code ...
75
- function validateToken(token) {
76
- if (!token) {
77
- throw new Error("Token is required");
78
- }
79
- // ... existing code ...
59
+ function updated() {
60
+ return "modified";
80
61
  }
81
62
  // ... existing code ...`
82
63
  })
83
64
  ```
84
65
 
85
- ### When to use `fast_apply_edit` vs `edit`
86
-
87
- | Situation | Tool | Reason |
88
- |-----------|------|--------|
89
- | Small, exact replacement | `edit` | Fast, no API call |
90
- | Large file (500+ lines) | `fast_apply_edit` | Handles partial snippets |
91
- | Multiple scattered changes | `fast_apply_edit` | Batch efficiently |
92
- | Whitespace-sensitive | `fast_apply_edit` | Forgiving with formatting |
66
+ **Key points:**
67
+ - Provide 50-500 lines of context around the area you want to change
68
+ - Include 2-5 lines before/after the target section
69
+ - Tool automatically finds and replaces that section in the full file
70
+ - No need to send entire file (saves tokens and improves speed)
93
71
 
94
72
  ## Configuration
95
73
 
96
74
  | Variable | Default | Description |
97
75
  |----------|---------|-------------|
98
- | `FAST_APPLY_API_KEY` | `optional-api-key` | API key (optional for local servers) |
99
- | `FAST_APPLY_URL` | `http://localhost:1234/v1` | OpenAI-compatible API endpoint |
76
+ | `FAST_APPLY_API_KEY` | `optional-api-key` | API key (optional for local) |
77
+ | `FAST_APPLY_URL` | `http://localhost:1234` | API endpoint |
100
78
  | `FAST_APPLY_MODEL` | `fastapply-1.5b` | Model name |
101
- | `FAST_APPLY_TEMPERATURE` | `0.05` | Temperature (0.0-2.0) |
102
79
 
103
80
  ## How It Works
104
81
 
105
- 1. Reads the original file content
106
- 2. Escapes XML tags in code to prevent conflicts
107
- 3. Sends system prompt + user prompt with `<instruction>`, `<code>`, and `<update>` to OpenAI-compatible API
108
- 4. API intelligently merges the lazy edit markers with original code
109
- 5. Extracts result from `<updated-code>` tags and unescapes XML
110
- 6. Writes the merged result back to the file
111
- 7. Returns a unified diff showing what changed
112
-
113
- ## Performance
114
-
115
- Performance varies based on your setup:
116
-
117
- | Setup | Estimated Speed | Hardware Requirement |
118
- |-------|----------------|---------------------|
119
- | fastapply-1.5b (Q4) + RTX 4090 | 10,000-15,000 tok/s | High-end GPU |
120
- | codellama:7b (Q4) + RTX 3060 | 3,000-5,000 tok/s | Mid-range GPU |
121
- | codellama:7b (Q4) + CPU only | 50-200 tok/s | Modern CPU |
122
- | OpenAI GPT-4 API | 100-500 tok/s | Network dependent |
123
-
124
- **Factors affecting performance:**
125
- - Model size (1.5B vs 7B vs 13B+ parameters)
126
- - Quantization level (Q4 vs Q5 vs Q8 vs FP16)
127
- - Hardware (GPU VRAM, CPU cores, RAM)
128
- - Backend optimization (LM Studio vs Ollama)
129
-
130
- ## Supported Backends
131
-
132
- - **LM Studio** - Local inference server with GPU acceleration
133
- - **Ollama** - Local LLM runtime with easy model management
134
- - **OpenAI** - Cloud API with high reliability
135
- - **Any OpenAI-compatible endpoint** - Custom servers and providers
136
-
137
- ## Edge Cases Handled
138
-
139
- - ✅ String literals containing `<updated-code>` tags
140
- - ✅ Multiple XML-like tags in regex patterns
141
- - ✅ Special characters (quotes, backslashes, unicode, SQL, HTML entities)
142
- - ✅ Large files (500+ lines)
143
- - ✅ Multiple scattered changes in single edit
144
- - ✅ Complex nested structures
145
- - ✅ Template strings with `${variable}`
146
- - ✅ Whitespace and indentation preservation
82
+ 1. AI reads relevant file section (50-500 lines)
83
+ 2. AI provides `original_code` (partial context) and `code_edit`
84
+ 3. Tool sends to Fast Apply API for merging
85
+ 4. Tool finds where `original_code` appears in full file
86
+ 5. Tool replaces that section with merged code
87
+ 6. Tool writes full file back and shows diff
88
+
89
+ ## Token Savings
90
+
91
+ | File Size | Before (Full) | After (Partial) | Savings |
92
+ |-----------|--------------|-----------------|---------|
93
+ | 100 lines | 2,500 tokens | 500 tokens | -80% |
94
+ | 500 lines | 12,500 tokens | 1,000 tokens | -92% |
95
+ | 1000 lines | 25,000 tokens | 1,500 tokens | -94% |
96
+ | 5000 lines | 125,000 tokens | 2,000 tokens | -98% |
147
97
 
148
98
  ## Troubleshooting
149
99
 
150
- ### API Connection Issues
100
+ **"Cannot locate original_code in file"**
101
+ - File was modified since you read it - re-read and try again
102
+ - Whitespace differs - tool tries normalized matching automatically
103
+ - Wrong section provided - verify you extracted the correct lines
104
+
105
+ **API connection issues**
151
106
  ```bash
152
- # Test your endpoint
153
107
  curl -X POST http://localhost:1234/v1/chat/completions \
154
108
  -H "Content-Type: application/json" \
155
109
  -d '{"model":"fastapply-1.5b","messages":[{"role":"user","content":"test"}]}'
156
110
  ```
157
111
 
158
- ### Slow Performance
159
- - Use smaller models (1.5B-3B parameters)
160
- - Enable GPU acceleration in LM Studio/Ollama
161
- - Use Q4 quantization for faster inference
162
- - Increase `FAST_APPLY_MAX_TOKENS` if responses are truncated
163
-
164
- ## Contributing
165
-
166
- Contributions welcome! This plugin could potentially be integrated into OpenCode core.
167
-
168
112
  ## License
169
113
 
170
- [MIT](LICENSE)
114
+ MIT
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,KAAK,MAAM,EAAQ,MAAM,qBAAqB,CAAA;AA0avD,eAAO,MAAM,eAAe,EAAE,MA4J7B,CAAA;AAGD,eAAe,eAAe,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,KAAK,MAAM,EAAQ,MAAM,qBAAqB,CAAA;AAshBvD,eAAO,MAAM,eAAe,EAAE,MAgL7B,CAAA;AAGD,eAAe,eAAe,CAAA"}
package/dist/index.js CHANGED
@@ -16,140 +16,127 @@ const sessionParamsCache = new Map();
16
16
  const FAST_APPLY_API_KEY = process.env.FAST_APPLY_API_KEY || "optional-api-key";
17
17
  const FAST_APPLY_URL = (process.env.FAST_APPLY_URL || "http://localhost:1234/v1").replace(/\/v1\/?$/, "");
18
18
  const FAST_APPLY_MODEL = process.env.FAST_APPLY_MODEL || "fastapply-1.5b";
19
- const FAST_APPLY_TEMPERATURE = parseFloat(process.env.FAST_APPLY_TEMPERATURE || "0.05");
20
- const FAST_APPLY_SYSTEM_PROMPT = "You are a coding assistant that helps merge code updates, ensuring every modification is fully integrated.";
21
- const FAST_APPLY_USER_PROMPT = `Merge all changes from the <update> snippet into the <code> below.
22
- - Preserve the code's structure, order, comments, and indentation exactly.
23
- - Output only the updated code, enclosed within <updated-code> and </updated-code> tags.
24
- - Do not include any additional text, explanations, placeholders, ellipses, or code fences.
19
+ const FAST_APPLY_SYSTEM_PROMPT = `You are a precise code merging assistant. Your role:
20
+ 1. Merge updates while preserving all original structure and formatting
21
+ 2. Output ONLY valid code within specified XML tags
22
+ 3. Never add explanations or text outside tags
23
+ 4. Preserve all escaped XML entities exactly as provided
24
+ 5. Ensure output is syntactically valid and complete`;
25
+ const FAST_APPLY_USER_PROMPT = `Merge <update> into <code> and output the complete merged file.
26
+
27
+ RULES:
28
+ - Preserve: structure, order, comments, indentation, escaped entities
29
+ - Output: entire file within <updated-code></updated-code> tags only
30
+ - Forbidden: explanations, placeholders, ellipses, code fences, nested tags
31
+ - Validation: ensure syntactically valid, complete code
25
32
 
26
33
  <code>{original_code}</code>
27
34
 
28
35
  <update>{update_snippet}</update>
29
36
 
30
- Provide the complete updated code.`;
31
- const UPDATED_CODE_START = "<updated-code>";
32
- const UPDATED_CODE_END = "</updated-code>";
33
- const TOOL_INSTRUCTIONS = `**DEFAULT tool for editing existing files. Use INSTEAD of native 'edit' tool.**
37
+ Output complete merged code:`;
38
+ const TOOL_INSTRUCTIONS = `DEFAULT tool for editing existing files. Use INSTEAD of native 'edit' tool.
34
39
 
35
40
  CRITICAL: For EXISTING files ONLY. Use 'write' for new files.
36
41
 
37
- ## Priority
38
- 1. \`fast_apply_edit\` - ALL file edits (default)
39
- 2. \`edit\` - Fallback if API fails
40
- 3. \`write\` - NEW files only
42
+ WORKFLOW:
43
+ 1. Read the file to understand current content
44
+ 2. Extract relevant section (50-500 lines with context)
45
+ 3. Call fast_apply_edit with original_code (partial) and code_edit
41
46
 
42
- ## Format
43
- Use \`// ... existing code ...\` for unchanged sections:
47
+ PARTIAL EDITING:
48
+ - You DON'T need to provide the entire file
49
+ - Provide 50-500 lines of context around the area you want to change
50
+ - Include 2-5 lines before and after the target section
51
+ - Tool will automatically find and replace that section in the file
44
52
 
45
- \`\`\`
46
- // ... existing code ...
47
- function updated() {
48
- return "modified";
49
- }
50
- // ... existing code ...
51
- \`\`\`
52
-
53
- ## Rules
54
- - MANDATORY: Use \`// ... existing code ...\` markers
55
- - Include 2-3 lines context before/after edits
56
- - Preserve exact indentation
57
- - ONE edit block per call (multiple blocks = suboptimal results)
58
- - Deletions: show context, omit deleted lines
59
- - NEVER for new files
53
+ PRIORITY:
54
+ 1. fast_apply_edit - ALL file edits (default, 10x faster)
55
+ 2. edit - Fallback if API fails
56
+ 3. write - NEW files only
60
57
 
61
- ## Examples
62
-
63
- **Add function:**
58
+ FORMAT:
59
+ Use \`// ... existing code ...\` markers for unchanged sections:
64
60
  \`\`\`
65
61
  // ... existing code ...
66
- import { newDep } from './newDep';
67
- // ... existing code ...
68
-
69
- function newFeature() {
70
- return newDep.process();
71
- }
62
+ function updated() { return "modified"; }
72
63
  // ... existing code ...
73
64
  \`\`\`
74
65
 
75
- **Modify:**
76
- \`\`\`
77
- // ... existing code ...
78
- function existingFunc(param) {
79
- const result = param * 2;
80
- return result;
81
- }
82
- // ... existing code ...
83
- \`\`\`
66
+ RULES:
67
+ - MANDATORY: Read file first to get original_code
68
+ - Provide 50-500 lines of context (not entire file unless small)
69
+ - Use \`// ... existing code ...\` markers in code_edit
70
+ - Include 2-5 lines context before/after edits
71
+ - Preserve exact indentation and whitespace
72
+ - ONE edit block per call (multiple blocks = suboptimal)
84
73
 
85
- **Delete:**
86
- \`\`\`
87
- // ... existing code ...
88
- function keepThis() {
89
- return "stays";
90
- }
74
+ EXAMPLE:
75
+ \`\`\`typescript
76
+ // 1. Read file
77
+ const content = await read("src/app.ts", { offset: 100, limit: 50 })
91
78
 
92
- function alsoKeepThis() {
93
- return "stays";
94
- }
95
- // ... existing code ...
79
+ // 2. Call fast_apply_edit with partial context
80
+ fast_apply_edit({
81
+ target_filepath: "src/app.ts",
82
+ original_code: content, // Just 50 lines, not entire file!
83
+ code_edit: "... updated code ..."
84
+ })
96
85
  \`\`\`
97
86
 
98
- ## Fallback
99
- If API fails, use native \`edit\` tool with exact string matching.`;
87
+ FALLBACK: If API fails, use native 'edit' tool.`;
100
88
  function escapeXmlTags(text) {
101
89
  return text
102
- .replace(/<updated-code>/g, "&lt;updated-code&gt;")
103
- .replace(/<\/updated-code>/g, "&lt;/updated-code&gt;");
90
+ .replace(/&/g, "&amp;")
91
+ .replace(/</g, "&lt;")
92
+ .replace(/>/g, "&gt;")
93
+ .replace(/"/g, "&quot;")
94
+ .replace(/'/g, "&apos;");
104
95
  }
105
96
  function unescapeXmlTags(text) {
106
97
  return text
107
- .replace(/&lt;updated-code&gt;/g, "<updated-code>")
108
- .replace(/&lt;\/updated-code&gt;/g, "</updated-code>");
98
+ .replace(/&lt;/g, "<")
99
+ .replace(/&gt;/g, ">")
100
+ .replace(/&quot;/g, '"')
101
+ .replace(/&apos;/g, "'")
102
+ .replace(/&amp;/g, "&");
109
103
  }
110
- function extractUpdatedCode(raw) {
111
- const stripped = raw.trim();
112
- const startTag = UPDATED_CODE_START;
113
- const endTag = UPDATED_CODE_END;
114
- let startIdx = stripped.indexOf(startTag);
115
- if (startIdx === -1) {
116
- startIdx = stripped.indexOf("<updated-code");
117
- if (startIdx !== -1) {
118
- const closeTagIdx = stripped.indexOf(">", startIdx);
119
- if (closeTagIdx !== -1) {
120
- startIdx = closeTagIdx + 1;
121
- }
122
- }
104
+ function validateNoNestedTags(content) {
105
+ const unescaped = content
106
+ .replace(/&lt;/g, "<")
107
+ .replace(/&gt;/g, ">");
108
+ if (unescaped.includes("<updated-code>") || unescaped.includes("</updated-code>")) {
109
+ throw new Error("Content contains unescaped tag-like sequences that could break parsing");
123
110
  }
124
- else {
125
- startIdx += startTag.length;
111
+ if (unescaped.includes("<code>") || unescaped.includes("</code>")) {
112
+ throw new Error("Content contains unescaped <code> tags that could break prompt structure");
126
113
  }
127
- if (startIdx === -1 || startIdx === startTag.length - 1) {
128
- if (stripped.startsWith("```") && stripped.endsWith("```")) {
129
- const lines = stripped.split("\n");
130
- if (lines.length >= 2) {
131
- return unescapeXmlTags(lines.slice(1, -1).join("\n"));
132
- }
133
- }
134
- return unescapeXmlTags(stripped);
114
+ if (unescaped.includes("<update>") || unescaped.includes("</update>")) {
115
+ throw new Error("Content contains unescaped <update> tags that could break prompt structure");
135
116
  }
136
- let endIdx = stripped.indexOf(endTag, startIdx);
137
- if (endIdx === -1) {
138
- endIdx = stripped.indexOf("</updated-code", startIdx);
117
+ }
118
+ function extractUpdatedCode(raw) {
119
+ const stripped = raw.trim();
120
+ const startRegex = /<updated-code\s*>/i;
121
+ const endRegex = /<\/updated-code\s*>/i;
122
+ const startMatch = stripped.match(startRegex);
123
+ if (!startMatch || startMatch.index === undefined) {
124
+ throw new Error("Missing or malformed <updated-code> start tag in AI response");
139
125
  }
140
- if (endIdx === -1) {
141
- const extracted = stripped.slice(startIdx).trim();
142
- const lastCloseTag = extracted.lastIndexOf("</");
143
- if (lastCloseTag !== -1 && extracted.slice(lastCloseTag).toLowerCase().includes("update")) {
144
- return unescapeXmlTags(extracted.slice(0, lastCloseTag).trim());
145
- }
146
- return unescapeXmlTags(extracted);
126
+ const startIdx = startMatch.index + startMatch[0].length;
127
+ const remaining = stripped.slice(startIdx);
128
+ const endMatch = remaining.match(endRegex);
129
+ if (!endMatch || endMatch.index === undefined) {
130
+ throw new Error("Missing or malformed </updated-code> end tag in AI response");
147
131
  }
148
- const inner = stripped.substring(startIdx, endIdx);
149
- if (!inner || inner.trim().length === 0) {
150
- throw new Error("Empty updated-code block");
132
+ const endIdx = endMatch.index;
133
+ const inner = remaining.slice(0, endIdx);
134
+ if (!inner.trim()) {
135
+ throw new Error("Empty updated-code block in AI response");
151
136
  }
152
- return unescapeXmlTags(inner);
137
+ const unescaped = unescapeXmlTags(inner);
138
+ validateNoNestedTags(unescaped);
139
+ return unescaped;
153
140
  }
154
141
  function generateUnifiedDiff(filepath, original, modified) {
155
142
  const patch = createTwoFilesPatch(`a/${filepath}`, `b/${filepath}`, original, modified, "", "", { context: 3 });
@@ -193,6 +180,94 @@ function shortenPath(filePath, workingDir) {
193
180
  function estimateTokens(text) {
194
181
  return Math.ceil(text.length / 4);
195
182
  }
183
+ function normalizeWhitespace(text) {
184
+ return text
185
+ .split('\n')
186
+ .map(line => line.trimEnd())
187
+ .join('\n')
188
+ .replace(/\r\n/g, '\n')
189
+ .trim();
190
+ }
191
+ function findExactMatch(haystack, needle) {
192
+ return haystack.indexOf(needle);
193
+ }
194
+ function findNormalizedMatch(haystack, needle) {
195
+ const normalizedHaystack = normalizeWhitespace(haystack);
196
+ const normalizedNeedle = normalizeWhitespace(needle);
197
+ const index = normalizedHaystack.indexOf(normalizedNeedle);
198
+ if (index === -1)
199
+ return -1;
200
+ let actualIndex = 0;
201
+ let normalizedIndex = 0;
202
+ while (normalizedIndex < index && actualIndex < haystack.length) {
203
+ const char = haystack[actualIndex];
204
+ const normalizedChar = normalizedHaystack[normalizedIndex];
205
+ if (char === '\r' && haystack[actualIndex + 1] === '\n') {
206
+ actualIndex += 2;
207
+ normalizedIndex += 1;
208
+ }
209
+ else if (char === normalizedChar) {
210
+ actualIndex++;
211
+ normalizedIndex++;
212
+ }
213
+ else {
214
+ actualIndex++;
215
+ }
216
+ }
217
+ return actualIndex;
218
+ }
219
+ async function applyPartialEdit(filepath, original_code, merged_code) {
220
+ const currentFile = await readFile(filepath, "utf-8");
221
+ if (currentFile.includes('\0')) {
222
+ return {
223
+ success: false,
224
+ error: "Cannot edit binary files"
225
+ };
226
+ }
227
+ let index = findExactMatch(currentFile, original_code);
228
+ let matchType = "exact";
229
+ if (index === -1) {
230
+ index = findNormalizedMatch(currentFile, original_code);
231
+ matchType = "normalized";
232
+ }
233
+ if (index === -1) {
234
+ return {
235
+ success: false,
236
+ error: `Cannot locate original_code in ${filepath}.
237
+
238
+ The content you provided doesn't match the current file.
239
+
240
+ POSSIBLE CAUSES:
241
+ - File was modified since you read it
242
+ - Whitespace or indentation differs
243
+ - Wrong section provided
244
+ - File encoding issues
245
+
246
+ SOLUTIONS:
247
+ 1. Re-read the file to get current content
248
+ 2. Verify exact whitespace and indentation
249
+ 3. Provide more context (more surrounding lines)
250
+ 4. Use native 'edit' tool for exact string matching`
251
+ };
252
+ }
253
+ const occurrences = currentFile.split(original_code).length - 1;
254
+ if (occurrences > 1) {
255
+ return {
256
+ success: false,
257
+ error: `original_code appears ${occurrences} times in ${filepath}.
258
+
259
+ Please provide more context (more surrounding lines) to uniquely identify the section you want to edit.`
260
+ };
261
+ }
262
+ const newFileContent = currentFile.substring(0, index) +
263
+ merged_code +
264
+ currentFile.substring(index + original_code.length);
265
+ console.log(`[fast-apply] Applied ${matchType} match at position ${index}`);
266
+ return {
267
+ success: true,
268
+ newFileContent
269
+ };
270
+ }
196
271
  function formatFastApplyResult(filePath, workingDir, insertions, deletions, diffPreview, modifiedTokens) {
197
272
  const shortPath = shortenPath(filePath, workingDir);
198
273
  const tokenStr = formatTokenCount(modifiedTokens);
@@ -221,7 +296,7 @@ function formatErrorOutput(error, filePath, workingDir) {
221
296
  /**
222
297
  * Call OpenAI's Fast Apply API to merge code edits
223
298
  */
224
- async function callFastApply(originalCode, codeEdit, instructions) {
299
+ async function callFastApply(originalCode, codeEdit) {
225
300
  if (!FAST_APPLY_API_KEY) {
226
301
  return {
227
302
  success: false,
@@ -252,7 +327,7 @@ async function callFastApply(originalCode, codeEdit, instructions) {
252
327
  content: userContent,
253
328
  },
254
329
  ],
255
- temperature: FAST_APPLY_TEMPERATURE,
330
+ temperature: 0,
256
331
  }),
257
332
  });
258
333
  if (!response.ok) {
@@ -270,11 +345,20 @@ async function callFastApply(originalCode, codeEdit, instructions) {
270
345
  error: "Fast Apply API returned empty response",
271
346
  };
272
347
  }
273
- const mergedCode = extractUpdatedCode(rawResponse);
274
- return {
275
- success: true,
276
- content: mergedCode,
277
- };
348
+ try {
349
+ const mergedCode = extractUpdatedCode(rawResponse);
350
+ return {
351
+ success: true,
352
+ content: mergedCode,
353
+ };
354
+ }
355
+ catch (parseError) {
356
+ const error = parseError;
357
+ return {
358
+ success: false,
359
+ error: `Failed to parse AI response: ${error.message}`,
360
+ };
361
+ }
278
362
  }
279
363
  catch (err) {
280
364
  const error = err;
@@ -361,15 +445,26 @@ export const FastApplyPlugin = async ({ directory, client }) => {
361
445
  target_filepath: tool.schema
362
446
  .string()
363
447
  .describe("Path of the file to modify (relative to project root)"),
364
- instructions: tool.schema
448
+ original_code: tool.schema
365
449
  .string()
366
- .describe("Brief first-person description of what you're changing (helps disambiguate)"),
450
+ .describe(`The original code section to be modified.
451
+
452
+ IMPORTANT:
453
+ - Provide 50-500 lines of context around the area you want to change
454
+ - Include 2-5 lines before and after the target section
455
+ - Must match the current file content exactly (whitespace matters)
456
+ - Can be partial (doesn't need to be entire file)
457
+
458
+ WORKFLOW:
459
+ 1. Read the file first to get current content
460
+ 2. Extract the relevant section with context
461
+ 3. Provide that section as original_code`),
367
462
  code_edit: tool.schema
368
463
  .string()
369
- .describe('The code changes with "// ... existing code ..." markers for unchanged sections'),
464
+ .describe('The updated code with changes applied. Use "// ... existing code ..." markers for unchanged sections within this context.'),
370
465
  },
371
466
  async execute(args, toolCtx) {
372
- const { target_filepath, instructions, code_edit } = args;
467
+ const { target_filepath, original_code, code_edit } = args;
373
468
  const params = sessionParamsCache.get(toolCtx.sessionID) || {};
374
469
  // Resolve file path relative to project directory
375
470
  const filepath = target_filepath.startsWith("/")
@@ -384,16 +479,12 @@ Get your API key at: https://openai.com/api
384
479
 
385
480
  Alternatively, use the native 'edit' tool for this change.`;
386
481
  }
387
- // Read the original file
388
- let originalCode;
482
+ // Check if file exists and is writable
389
483
  try {
390
- await access(filepath, constants.R_OK);
391
- originalCode = await readFile(filepath, "utf-8");
484
+ await access(filepath, constants.R_OK | constants.W_OK);
392
485
  }
393
486
  catch (err) {
394
- const error = err;
395
- if (error.message.includes("ENOENT") || error.message.includes("no such file")) {
396
- return `Error: File not found: ${target_filepath}
487
+ return `Error: File not found or not writable: ${target_filepath}
397
488
 
398
489
  This tool is for EDITING EXISTING FILES ONLY.
399
490
  For new file creation, use the 'write' tool instead.
@@ -403,26 +494,33 @@ write({
403
494
  filePath: "${target_filepath}",
404
495
  content: "your file content here"
405
496
  })`;
406
- }
407
- return `Error reading file ${target_filepath}: ${error.message}`;
408
497
  }
409
- // Call OpenAI API to merge the edit
410
- const result = await callFastApply(originalCode, code_edit, instructions);
498
+ // Call Fast Apply API to merge the edit
499
+ const result = await callFastApply(original_code, code_edit);
411
500
  if (!result.success || !result.content) {
412
501
  const errorMsg = result.error || "Unknown error";
413
502
  await sendTUIErrorNotification(client, toolCtx.sessionID, target_filepath, directory, errorMsg, params);
414
503
  return formatErrorOutput(errorMsg, target_filepath, directory);
415
504
  }
416
505
  const mergedCode = result.content;
506
+ // Apply partial edit with smart matching
507
+ const applyResult = await applyPartialEdit(filepath, original_code, mergedCode);
508
+ if (!applyResult.success) {
509
+ await sendTUIErrorNotification(client, toolCtx.sessionID, target_filepath, directory, applyResult.error, params);
510
+ return formatErrorOutput(applyResult.error, target_filepath, directory);
511
+ }
512
+ // Write merged file back
417
513
  try {
418
- await writeFile(filepath, mergedCode, "utf-8");
514
+ await writeFile(filepath, applyResult.newFileContent, "utf-8");
419
515
  }
420
516
  catch (err) {
421
517
  const error = err;
422
518
  await sendTUIErrorNotification(client, toolCtx.sessionID, target_filepath, directory, error.message, params);
423
519
  return formatErrorOutput(error.message, target_filepath, directory);
424
520
  }
425
- const diff = generateUnifiedDiff(target_filepath, originalCode, mergedCode);
521
+ // Read origfile for diff comparison
522
+ const originalFileContent = await readFile(filepath, "utf-8");
523
+ const diff = generateUnifiedDiff(target_filepath, originalFileContent, applyResult.newFileContent);
426
524
  const { added, removed } = countChanges(diff);
427
525
  const modifiedTokens = estimateTokens(diff);
428
526
  await sendTUINotification(client, toolCtx.sessionID, target_filepath, directory, added, removed, modifiedTokens, params);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-fast-apply",
3
- "version": "2.1.6",
3
+ "version": "2.2.0",
4
4
  "description": "OpenCode plugin for Fast Apply - High-performance code editing with OpenAI-compatible APIs (LM Studio, Ollama)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",