opencode-fast-apply 2.1.0 → 2.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 +3 -15
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +113 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,35 +20,25 @@ OpenCode plugin for Fast Apply - High-performance code editing with OpenAI-compa
|
|
|
20
20
|
npm install -g opencode-fast-apply
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
Or add to your OpenCode config to auto-install:
|
|
24
|
-
|
|
25
|
-
```json
|
|
26
|
-
{
|
|
27
|
-
"plugin": [
|
|
28
|
-
"opencode-fast-apply"
|
|
29
|
-
]
|
|
30
|
-
}
|
|
31
|
-
```
|
|
32
|
-
|
|
33
23
|
### 2. Configure your API endpoint
|
|
34
24
|
|
|
35
25
|
For **LM Studio** (default):
|
|
36
26
|
```bash
|
|
37
|
-
export FAST_APPLY_URL="http://localhost:1234
|
|
27
|
+
export FAST_APPLY_URL="http://localhost:1234"
|
|
38
28
|
export FAST_APPLY_MODEL="fastapply-1.5b"
|
|
39
29
|
export FAST_APPLY_API_KEY="optional-api-key"
|
|
40
30
|
```
|
|
41
31
|
|
|
42
32
|
For **Ollama**:
|
|
43
33
|
```bash
|
|
44
|
-
export FAST_APPLY_URL="http://localhost:11434
|
|
34
|
+
export FAST_APPLY_URL="http://localhost:11434"
|
|
45
35
|
export FAST_APPLY_MODEL="codellama:7b"
|
|
46
36
|
export FAST_APPLY_API_KEY="optional-api-key"
|
|
47
37
|
```
|
|
48
38
|
|
|
49
39
|
For **OpenAI**:
|
|
50
40
|
```bash
|
|
51
|
-
export FAST_APPLY_URL="https://api.openai.com
|
|
41
|
+
export FAST_APPLY_URL="https://api.openai.com"
|
|
52
42
|
export FAST_APPLY_MODEL="gpt-4"
|
|
53
43
|
export FAST_APPLY_API_KEY="sk-your-openai-key"
|
|
54
44
|
```
|
|
@@ -62,7 +52,6 @@ Add to your global config (`~/.config/opencode/opencode.json` or `opencode.jsonc
|
|
|
62
52
|
```json
|
|
63
53
|
{
|
|
64
54
|
"plugin": [
|
|
65
|
-
"opencode-pty",
|
|
66
55
|
"opencode-fast-apply"
|
|
67
56
|
]
|
|
68
57
|
}
|
|
@@ -111,7 +100,6 @@ function validateToken(token) {
|
|
|
111
100
|
| `FAST_APPLY_MODEL` | `fastapply-1.5b` | Model name |
|
|
112
101
|
| `FAST_APPLY_TIMEOUT` | `30000` | Request timeout in ms |
|
|
113
102
|
| `FAST_APPLY_TEMPERATURE` | `0.05` | Temperature (0.0-2.0) |
|
|
114
|
-
| `FAST_APPLY_MAX_TOKENS` | `8000` | Maximum tokens in response |
|
|
115
103
|
|
|
116
104
|
## How It Works
|
|
117
105
|
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,KAAK,MAAM,EAAQ,MAAM,qBAAqB,CAAA;AAwbvD,eAAO,MAAM,eAAe,EAAE,MA4J7B,CAAA;AAGD,eAAe,eAAe,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -11,20 +11,24 @@ import { tool } from "@opencode-ai/plugin";
|
|
|
11
11
|
import { createTwoFilesPatch } from "diff";
|
|
12
12
|
import { readFile, writeFile, access } from "fs/promises";
|
|
13
13
|
import { constants } from "fs";
|
|
14
|
+
const sessionParamsCache = new Map();
|
|
14
15
|
// Get API key from environment (set in mcpm/jarvis config)
|
|
15
16
|
const FAST_APPLY_API_KEY = process.env.FAST_APPLY_API_KEY || "optional-api-key";
|
|
16
17
|
const FAST_APPLY_URL = (process.env.FAST_APPLY_URL || "http://localhost:1234/v1").replace(/\/v1\/?$/, "");
|
|
17
18
|
const FAST_APPLY_MODEL = process.env.FAST_APPLY_MODEL || "fastapply-1.5b";
|
|
18
19
|
const FAST_APPLY_TIMEOUT = parseInt(process.env.FAST_APPLY_TIMEOUT || "30000", 10);
|
|
19
20
|
const FAST_APPLY_TEMPERATURE = parseFloat(process.env.FAST_APPLY_TEMPERATURE || "0.05");
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
21
|
+
const FAST_APPLY_SYSTEM_PROMPT = "You are a coding assistant that helps merge code updates, ensuring every modification is fully integrated.";
|
|
22
|
+
const FAST_APPLY_USER_PROMPT = `Merge all changes from the <update> snippet into the <code> below.
|
|
23
|
+
- Preserve the code's structure, order, comments, and indentation exactly.
|
|
24
|
+
- Output only the updated code, enclosed within <updated-code> and </updated-code> tags.
|
|
25
|
+
- Do not include any additional text, explanations, placeholders, ellipses, or code fences.
|
|
23
26
|
|
|
24
27
|
<code>{original_code}</code>
|
|
28
|
+
|
|
25
29
|
<update>{update_snippet}</update>
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
Provide the complete updated code.`;
|
|
28
32
|
const UPDATED_CODE_START = "<updated-code>";
|
|
29
33
|
const UPDATED_CODE_END = "</updated-code>";
|
|
30
34
|
const TOOL_INSTRUCTIONS = `**DEFAULT tool for editing existing files. Use INSTEAD of native 'edit' tool.**
|
|
@@ -106,9 +110,22 @@ function unescapeXmlTags(text) {
|
|
|
106
110
|
}
|
|
107
111
|
function extractUpdatedCode(raw) {
|
|
108
112
|
const stripped = raw.trim();
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
|
|
113
|
+
const startTag = UPDATED_CODE_START;
|
|
114
|
+
const endTag = UPDATED_CODE_END;
|
|
115
|
+
let startIdx = stripped.indexOf(startTag);
|
|
116
|
+
if (startIdx === -1) {
|
|
117
|
+
startIdx = stripped.indexOf("<updated-code");
|
|
118
|
+
if (startIdx !== -1) {
|
|
119
|
+
const closeTagIdx = stripped.indexOf(">", startIdx);
|
|
120
|
+
if (closeTagIdx !== -1) {
|
|
121
|
+
startIdx = closeTagIdx + 1;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
startIdx += startTag.length;
|
|
127
|
+
}
|
|
128
|
+
if (startIdx === -1 || startIdx === startTag.length - 1) {
|
|
112
129
|
if (stripped.startsWith("```") && stripped.endsWith("```")) {
|
|
113
130
|
const lines = stripped.split("\n");
|
|
114
131
|
if (lines.length >= 2) {
|
|
@@ -117,7 +134,19 @@ function extractUpdatedCode(raw) {
|
|
|
117
134
|
}
|
|
118
135
|
return unescapeXmlTags(stripped);
|
|
119
136
|
}
|
|
120
|
-
|
|
137
|
+
let endIdx = stripped.indexOf(endTag, startIdx);
|
|
138
|
+
if (endIdx === -1) {
|
|
139
|
+
endIdx = stripped.indexOf("</updated-code", startIdx);
|
|
140
|
+
}
|
|
141
|
+
if (endIdx === -1) {
|
|
142
|
+
const extracted = stripped.slice(startIdx).trim();
|
|
143
|
+
const lastCloseTag = extracted.lastIndexOf("</");
|
|
144
|
+
if (lastCloseTag !== -1 && extracted.slice(lastCloseTag).toLowerCase().includes("update")) {
|
|
145
|
+
return unescapeXmlTags(extracted.slice(0, lastCloseTag).trim());
|
|
146
|
+
}
|
|
147
|
+
return unescapeXmlTags(extracted);
|
|
148
|
+
}
|
|
149
|
+
const inner = stripped.substring(startIdx, endIdx);
|
|
121
150
|
if (!inner || inner.trim().length === 0) {
|
|
122
151
|
throw new Error("Empty updated-code block");
|
|
123
152
|
}
|
|
@@ -162,30 +191,20 @@ function shortenPath(filePath, workingDir) {
|
|
|
162
191
|
}
|
|
163
192
|
return filePath;
|
|
164
193
|
}
|
|
165
|
-
function truncate(str, maxLen = 80) {
|
|
166
|
-
if (str.length <= maxLen)
|
|
167
|
-
return str;
|
|
168
|
-
return str.slice(0, maxLen - 3) + "...";
|
|
169
|
-
}
|
|
170
194
|
function estimateTokens(text) {
|
|
171
195
|
return Math.ceil(text.length / 4);
|
|
172
196
|
}
|
|
173
197
|
function formatFastApplyResult(filePath, workingDir, insertions, deletions, diffPreview, modifiedTokens) {
|
|
174
198
|
const shortPath = shortenPath(filePath, workingDir);
|
|
175
199
|
const tokenStr = formatTokenCount(modifiedTokens);
|
|
176
|
-
const diffLines = diffPreview.split("\n");
|
|
177
|
-
const previewLines = diffLines.slice(0, 10);
|
|
178
|
-
const truncatedDiff = previewLines.join("\n");
|
|
179
|
-
const hasMore = diffLines.length > 10;
|
|
180
200
|
const lines = [
|
|
181
201
|
"✓ Fast Apply complete",
|
|
182
202
|
"",
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
` +${insertions} lines, -${deletions} lines (~${tokenStr} tokens modified)`,
|
|
203
|
+
`File: ${shortPath}`,
|
|
204
|
+
`Changes: +${insertions} -${deletions} (~${tokenStr} tokens)`,
|
|
186
205
|
"",
|
|
187
|
-
"
|
|
188
|
-
|
|
206
|
+
"Unified diff:",
|
|
207
|
+
diffPreview
|
|
189
208
|
];
|
|
190
209
|
return lines.join("\n");
|
|
191
210
|
}
|
|
@@ -194,8 +213,8 @@ function formatErrorOutput(error, filePath, workingDir) {
|
|
|
194
213
|
return [
|
|
195
214
|
"✗ Fast Apply failed",
|
|
196
215
|
"",
|
|
197
|
-
|
|
198
|
-
`
|
|
216
|
+
`File: ${shortPath}`,
|
|
217
|
+
`Error: ${error}`,
|
|
199
218
|
"",
|
|
200
219
|
"Fallback: Use native 'edit' tool with exact string matching"
|
|
201
220
|
].join("\n");
|
|
@@ -216,7 +235,6 @@ async function callFastApply(originalCode, codeEdit, instructions) {
|
|
|
216
235
|
const escapedOriginalCode = escapeXmlTags(originalCode);
|
|
217
236
|
const escapedCodeEdit = escapeXmlTags(codeEdit);
|
|
218
237
|
const userContent = FAST_APPLY_USER_PROMPT
|
|
219
|
-
.replace("{instruction}", instructions || "Apply the requested code changes.")
|
|
220
238
|
.replace("{original_code}", escapedOriginalCode)
|
|
221
239
|
.replace("{update_snippet}", escapedCodeEdit);
|
|
222
240
|
const response = await fetch(`${FAST_APPLY_URL}/v1/chat/completions`, {
|
|
@@ -238,7 +256,6 @@ async function callFastApply(originalCode, codeEdit, instructions) {
|
|
|
238
256
|
},
|
|
239
257
|
],
|
|
240
258
|
temperature: FAST_APPLY_TEMPERATURE,
|
|
241
|
-
max_tokens: FAST_APPLY_MAX_TOKENS,
|
|
242
259
|
}),
|
|
243
260
|
signal: controller.signal,
|
|
244
261
|
});
|
|
@@ -279,7 +296,61 @@ async function callFastApply(originalCode, codeEdit, instructions) {
|
|
|
279
296
|
};
|
|
280
297
|
}
|
|
281
298
|
}
|
|
282
|
-
|
|
299
|
+
async function sendTUIMessage(client, sessionID, message, params) {
|
|
300
|
+
const agent = params.agent || undefined;
|
|
301
|
+
const variant = params.variant || undefined;
|
|
302
|
+
const model = params.providerId && params.modelId
|
|
303
|
+
? {
|
|
304
|
+
providerID: params.providerId,
|
|
305
|
+
modelID: params.modelId,
|
|
306
|
+
}
|
|
307
|
+
: undefined;
|
|
308
|
+
try {
|
|
309
|
+
await client.session.prompt({
|
|
310
|
+
path: { id: sessionID },
|
|
311
|
+
body: {
|
|
312
|
+
noReply: true,
|
|
313
|
+
agent: agent,
|
|
314
|
+
model: model,
|
|
315
|
+
variant: variant,
|
|
316
|
+
parts: [
|
|
317
|
+
{
|
|
318
|
+
type: "text",
|
|
319
|
+
text: message,
|
|
320
|
+
ignored: true,
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
console.error("[fast-apply] Failed to send TUI notification:", error.message);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
async function sendTUINotification(client, sessionID, filePath, workingDir, insertions, deletions, modifiedTokens, params) {
|
|
331
|
+
const shortPath = shortenPath(filePath, workingDir);
|
|
332
|
+
const tokenStr = formatTokenCount(modifiedTokens);
|
|
333
|
+
const message = [
|
|
334
|
+
`▣ Fast Apply | ~${tokenStr} tokens modified`,
|
|
335
|
+
"",
|
|
336
|
+
"Applied changes:",
|
|
337
|
+
`→ ${shortPath}: +${insertions} -${deletions}`
|
|
338
|
+
].join("\n");
|
|
339
|
+
await sendTUIMessage(client, sessionID, message, params);
|
|
340
|
+
}
|
|
341
|
+
async function sendTUIErrorNotification(client, sessionID, filePath, workingDir, errorMessage, params) {
|
|
342
|
+
const shortPath = shortenPath(filePath, workingDir);
|
|
343
|
+
const message = [
|
|
344
|
+
`✗ Fast Apply Error`,
|
|
345
|
+
"",
|
|
346
|
+
`File: ${shortPath}`,
|
|
347
|
+
`Error: ${errorMessage}`,
|
|
348
|
+
"",
|
|
349
|
+
"Fallback: Use native 'edit' tool"
|
|
350
|
+
].join("\n");
|
|
351
|
+
await sendTUIMessage(client, sessionID, message, params);
|
|
352
|
+
}
|
|
353
|
+
export const FastApplyPlugin = async ({ directory, client }) => {
|
|
283
354
|
if (!FAST_APPLY_API_KEY) {
|
|
284
355
|
console.warn("[fast-apply] FAST_APPLY_API_KEY not set - fast_apply_edit tool will be disabled");
|
|
285
356
|
}
|
|
@@ -287,6 +358,14 @@ export const FastApplyPlugin = async ({ directory }) => {
|
|
|
287
358
|
console.log(`[fast-apply] Plugin loaded with model: ${FAST_APPLY_MODEL} at ${FAST_APPLY_URL}`);
|
|
288
359
|
}
|
|
289
360
|
return {
|
|
361
|
+
"chat.message": async (input) => {
|
|
362
|
+
sessionParamsCache.set(input.sessionID, {
|
|
363
|
+
agent: input.agent,
|
|
364
|
+
providerId: input.model?.providerID,
|
|
365
|
+
modelId: input.model?.modelID,
|
|
366
|
+
variant: input.variant,
|
|
367
|
+
});
|
|
368
|
+
},
|
|
290
369
|
tool: {
|
|
291
370
|
fast_apply_edit: tool({
|
|
292
371
|
description: TOOL_INSTRUCTIONS,
|
|
@@ -301,8 +380,9 @@ export const FastApplyPlugin = async ({ directory }) => {
|
|
|
301
380
|
.string()
|
|
302
381
|
.describe('The code changes with "// ... existing code ..." markers for unchanged sections'),
|
|
303
382
|
},
|
|
304
|
-
async execute(args) {
|
|
383
|
+
async execute(args, toolCtx) {
|
|
305
384
|
const { target_filepath, instructions, code_edit } = args;
|
|
385
|
+
const params = sessionParamsCache.get(toolCtx.sessionID) || {};
|
|
306
386
|
// Resolve file path relative to project directory
|
|
307
387
|
const filepath = target_filepath.startsWith("/")
|
|
308
388
|
? target_filepath
|
|
@@ -341,7 +421,9 @@ write({
|
|
|
341
421
|
// Call OpenAI API to merge the edit
|
|
342
422
|
const result = await callFastApply(originalCode, code_edit, instructions);
|
|
343
423
|
if (!result.success || !result.content) {
|
|
344
|
-
|
|
424
|
+
const errorMsg = result.error || "Unknown error";
|
|
425
|
+
await sendTUIErrorNotification(client, toolCtx.sessionID, target_filepath, directory, errorMsg, params);
|
|
426
|
+
return formatErrorOutput(errorMsg, target_filepath, directory);
|
|
345
427
|
}
|
|
346
428
|
const mergedCode = result.content;
|
|
347
429
|
try {
|
|
@@ -349,11 +431,13 @@ write({
|
|
|
349
431
|
}
|
|
350
432
|
catch (err) {
|
|
351
433
|
const error = err;
|
|
434
|
+
await sendTUIErrorNotification(client, toolCtx.sessionID, target_filepath, directory, error.message, params);
|
|
352
435
|
return formatErrorOutput(error.message, target_filepath, directory);
|
|
353
436
|
}
|
|
354
437
|
const diff = generateUnifiedDiff(target_filepath, originalCode, mergedCode);
|
|
355
438
|
const { added, removed } = countChanges(diff);
|
|
356
439
|
const modifiedTokens = estimateTokens(diff);
|
|
440
|
+
await sendTUINotification(client, toolCtx.sessionID, target_filepath, directory, added, removed, modifiedTokens, params);
|
|
357
441
|
return formatFastApplyResult(target_filepath, directory, added, removed, diff, modifiedTokens);
|
|
358
442
|
},
|
|
359
443
|
}),
|
package/package.json
CHANGED