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 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/v1"
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/v1"
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/v1"
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
 
@@ -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;AA2UvD,eAAO,MAAM,eAAe,EAAE,MAiH7B,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;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 FAST_APPLY_MAX_TOKENS = parseInt(process.env.FAST_APPLY_MAX_TOKENS || "8000", 10);
21
- const FAST_APPLY_SYSTEM_PROMPT = "Merge code edits into original files. Preserve structure, indentation, and comments exactly.";
22
- const FAST_APPLY_USER_PROMPT = `Task: {instruction}
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
- Output complete merged code in <updated-code></updated-code> tags. No explanations, markdown, or ellipses.`;
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 start = stripped.indexOf(UPDATED_CODE_START);
110
- const end = stripped.lastIndexOf(UPDATED_CODE_END);
111
- if (start === -1 || end === -1 || end <= start) {
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
- const inner = stripped.substring(start + UPDATED_CODE_START.length, end);
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
- "Applied changes:",
184
- `→ ${shortPath}`,
185
- ` +${insertions} lines, -${deletions} lines (~${tokenStr} tokens modified)`,
203
+ `File: ${shortPath}`,
204
+ `Changes: +${insertions} -${deletions} (~${tokenStr} tokens)`,
186
205
  "",
187
- "Diff preview (first 10 lines):",
188
- truncatedDiff + (hasMore ? "\n..." : "")
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
- `→ ${shortPath}`,
198
- ` Error: ${truncate(error, 100)}`,
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
- export const FastApplyPlugin = async ({ directory }) => {
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
- return formatErrorOutput(result.error || "Unknown error", target_filepath, directory);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-fast-apply",
3
- "version": "2.1.0",
3
+ "version": "2.1.3",
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",