opencode-fast-apply 2.1.6 → 2.1.7

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
@@ -9,8 +9,9 @@ OpenCode plugin for Fast Apply - High-performance code editing with OpenAI-compa
9
9
  - **Unified diff output** with context for easy review
10
10
  - **Graceful fallback** - suggests native `edit` tool on API failure
11
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
12
+ - **Unique delimiter system** - generates random delimiters per request (`<<<RESULT_xxxx>>>`) to eliminate parsing conflicts
13
+ - **Zero escaping overhead** - no XML tag processing needed, handles all code patterns safely
14
+ - **Collision-resistant parsing** - 1.6M+ unique delimiter combinations prevent conflicts with file content
14
15
 
15
16
  ## Installation
16
17
 
@@ -103,13 +104,15 @@ function validateToken(token) {
103
104
  ## How It Works
104
105
 
105
106
  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
107
+ 2. Generates unique random delimiters for this request (e.g., `<<<RESULT_a3f9>>>`)
108
+ 3. Sends system prompt + user prompt with unique delimiters to OpenAI-compatible API
108
109
  4. API intelligently merges the lazy edit markers with original code
109
- 5. Extracts result from `<updated-code>` tags and unescapes XML
110
+ 5. Extracts result using the unique delimiters with fallback pattern detection
110
111
  6. Writes the merged result back to the file
111
112
  7. Returns a unified diff showing what changed
112
113
 
114
+ **Delimiter Design:** Each request generates unique 4-character alphanumeric IDs (1,679,616 combinations) for delimiters like `<<<RESULT_xxxx>>>` and `<<<END_RESULT_xxxx>>>`. This eliminates parsing conflicts even if your code contains similar patterns. Fallback logic detects any `<<<RESULT_*>>>` pattern for maximum robustness.
115
+
113
116
  ## Performance
114
117
 
115
118
  Performance varies based on your setup:
@@ -136,7 +139,8 @@ Performance varies based on your setup:
136
139
 
137
140
  ## Edge Cases Handled
138
141
 
139
- - ✅ String literals containing `<updated-code>` tags
142
+ - ✅ Code containing XML-like tags (`<update>`, `<code>`, `<result>`) in strings
143
+ - ✅ Code containing triple-angle-bracket patterns (`<<<RESULT>>>`) in comments or strings
140
144
  - ✅ Multiple XML-like tags in regex patterns
141
145
  - ✅ Special characters (quotes, backslashes, unicode, SQL, HTML entities)
142
146
  - ✅ Large files (500+ lines)
@@ -144,6 +148,8 @@ Performance varies based on your setup:
144
148
  - ✅ Complex nested structures
145
149
  - ✅ Template strings with `${variable}`
146
150
  - ✅ Whitespace and indentation preservation
151
+ - ✅ Unique delimiters per request prevent all parsing conflicts (1.6M+ combinations)
152
+ - ✅ Fallback pattern detection for robust delimiter parsing
147
153
 
148
154
  ## Troubleshooting
149
155
 
@@ -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;AAgbvD,eAAO,MAAM,eAAe,EAAE,MA4J7B,CAAA;AAGD,eAAe,eAAe,CAAA"}
package/dist/index.js CHANGED
@@ -18,18 +18,6 @@ const FAST_APPLY_URL = (process.env.FAST_APPLY_URL || "http://localhost:1234/v1"
18
18
  const FAST_APPLY_MODEL = process.env.FAST_APPLY_MODEL || "fastapply-1.5b";
19
19
  const FAST_APPLY_TEMPERATURE = parseFloat(process.env.FAST_APPLY_TEMPERATURE || "0.05");
20
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.
25
-
26
- <code>{original_code}</code>
27
-
28
- <update>{update_snippet}</update>
29
-
30
- Provide the complete updated code.`;
31
- const UPDATED_CODE_START = "<updated-code>";
32
- const UPDATED_CODE_END = "</updated-code>";
33
21
  const TOOL_INSTRUCTIONS = `**DEFAULT tool for editing existing files. Use INSTEAD of native 'edit' tool.**
34
22
 
35
23
  CRITICAL: For EXISTING files ONLY. Use 'write' for new files.
@@ -97,59 +85,69 @@ function alsoKeepThis() {
97
85
 
98
86
  ## Fallback
99
87
  If API fails, use native \`edit\` tool with exact string matching.`;
100
- function escapeXmlTags(text) {
101
- return text
102
- .replace(/<updated-code>/g, "&lt;updated-code&gt;")
103
- .replace(/<\/updated-code>/g, "&lt;/updated-code&gt;");
88
+ function generateRandomId() {
89
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
90
+ let result = '';
91
+ for (let i = 0; i < 4; i++) {
92
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
93
+ }
94
+ return result;
104
95
  }
105
- function unescapeXmlTags(text) {
106
- return text
107
- .replace(/&lt;updated-code&gt;/g, "<updated-code>")
108
- .replace(/&lt;\/updated-code&gt;/g, "</updated-code>");
96
+ function generateUniqueDelimiters() {
97
+ const id = generateRandomId();
98
+ return {
99
+ ORIGINAL_START: `<<<ORIGINAL_${id}>>>`,
100
+ ORIGINAL_END: `<<<END_ORIGINAL_${id}>>>`,
101
+ UPDATE_START: `<<<UPDATE_${id}>>>`,
102
+ UPDATE_END: `<<<END_UPDATE_${id}>>>`,
103
+ RESULT_START: `<<<RESULT_${id}>>>`,
104
+ RESULT_END: `<<<END_RESULT_${id}>>>`
105
+ };
109
106
  }
110
- function extractUpdatedCode(raw) {
107
+ function extractUpdatedCode(raw, resultStart, resultEnd) {
111
108
  const stripped = raw.trim();
112
- const startTag = UPDATED_CODE_START;
113
- const endTag = UPDATED_CODE_END;
114
- let startIdx = stripped.indexOf(startTag);
109
+ let startIdx = stripped.indexOf(resultStart);
115
110
  if (startIdx === -1) {
116
- startIdx = stripped.indexOf("<updated-code");
117
- if (startIdx !== -1) {
118
- const closeTagIdx = stripped.indexOf(">", startIdx);
111
+ const genericStart = stripped.indexOf("<<<RESULT_");
112
+ if (genericStart !== -1) {
113
+ const closeTagIdx = stripped.indexOf(">>>", genericStart);
119
114
  if (closeTagIdx !== -1) {
120
- startIdx = closeTagIdx + 1;
115
+ startIdx = closeTagIdx + 3;
121
116
  }
122
117
  }
123
118
  }
124
119
  else {
125
- startIdx += startTag.length;
120
+ startIdx += resultStart.length;
126
121
  }
127
- if (startIdx === -1 || startIdx === startTag.length - 1) {
122
+ if (startIdx === -1 || startIdx === resultStart.length - 1) {
128
123
  if (stripped.startsWith("```") && stripped.endsWith("```")) {
129
124
  const lines = stripped.split("\n");
130
125
  if (lines.length >= 2) {
131
- return unescapeXmlTags(lines.slice(1, -1).join("\n"));
126
+ return lines.slice(1, -1).join("\n");
132
127
  }
133
128
  }
134
- return unescapeXmlTags(stripped);
129
+ return stripped;
135
130
  }
136
- let endIdx = stripped.indexOf(endTag, startIdx);
131
+ let endIdx = stripped.indexOf(resultEnd, startIdx);
137
132
  if (endIdx === -1) {
138
- endIdx = stripped.indexOf("</updated-code", startIdx);
133
+ const genericEnd = stripped.indexOf("<<<END_RESULT_", startIdx);
134
+ if (genericEnd !== -1) {
135
+ endIdx = genericEnd;
136
+ }
139
137
  }
140
138
  if (endIdx === -1) {
141
139
  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());
140
+ const lastCloseTag = extracted.lastIndexOf("<<<");
141
+ if (lastCloseTag !== -1 && extracted.slice(lastCloseTag).toLowerCase().includes("end")) {
142
+ return extracted.slice(0, lastCloseTag).trim();
145
143
  }
146
- return unescapeXmlTags(extracted);
144
+ return extracted;
147
145
  }
148
146
  const inner = stripped.substring(startIdx, endIdx);
149
147
  if (!inner || inner.trim().length === 0) {
150
- throw new Error("Empty updated-code block");
148
+ throw new Error("Empty result block");
151
149
  }
152
- return unescapeXmlTags(inner);
150
+ return inner;
153
151
  }
154
152
  function generateUnifiedDiff(filepath, original, modified) {
155
153
  const patch = createTwoFilesPatch(`a/${filepath}`, `b/${filepath}`, original, modified, "", "", { context: 3 });
@@ -229,11 +227,21 @@ async function callFastApply(originalCode, codeEdit, instructions) {
229
227
  };
230
228
  }
231
229
  try {
232
- const escapedOriginalCode = escapeXmlTags(originalCode);
233
- const escapedCodeEdit = escapeXmlTags(codeEdit);
234
- const userContent = FAST_APPLY_USER_PROMPT
235
- .replace("{original_code}", escapedOriginalCode)
236
- .replace("{update_snippet}", escapedCodeEdit);
230
+ const delimiters = generateUniqueDelimiters();
231
+ const userContent = `Merge all changes from the UPDATE_BLOCK into the ORIGINAL_BLOCK below.
232
+ - Preserve the code's structure, order, comments, and indentation exactly.
233
+ - Output only the updated code, enclosed within ${delimiters.RESULT_START} and ${delimiters.RESULT_END} delimiters.
234
+ - Do not include any additional text, explanations, placeholders, ellipses, or code fences.
235
+
236
+ ${delimiters.ORIGINAL_START}
237
+ ${originalCode}
238
+ ${delimiters.ORIGINAL_END}
239
+
240
+ ${delimiters.UPDATE_START}
241
+ ${codeEdit}
242
+ ${delimiters.UPDATE_END}
243
+
244
+ Provide the complete updated code wrapped in ${delimiters.RESULT_START} and ${delimiters.RESULT_END}.`;
237
245
  const response = await fetch(`${FAST_APPLY_URL}/v1/chat/completions`, {
238
246
  method: "POST",
239
247
  headers: {
@@ -270,7 +278,7 @@ async function callFastApply(originalCode, codeEdit, instructions) {
270
278
  error: "Fast Apply API returned empty response",
271
279
  };
272
280
  }
273
- const mergedCode = extractUpdatedCode(rawResponse);
281
+ const mergedCode = extractUpdatedCode(rawResponse, delimiters.RESULT_START, delimiters.RESULT_END);
274
282
  return {
275
283
  success: true,
276
284
  content: mergedCode,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-fast-apply",
3
- "version": "2.1.6",
3
+ "version": "2.1.7",
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",