opencode-minimax-easy-vision 1.0.0 → 1.1.1

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,6 +1,6 @@
1
- # MiniMax Easy Vision
1
+ # Opencode MiniMax Easy Vision
2
2
 
3
- MiniMax Easy Vision is a plugin for [OpenCode](https://opencode.ai) that enables **vision support** when using [MiniMax](https://www.minimax.io/) models. It restores a simple paste and ask workflow by automatically handling image assets and routing them through the [MiniMax Coding Plan MCP](https://github.com/MiniMax-AI/MiniMax-Coding-Plan-MCP)
3
+ MiniMax Easy Vision is a plugin for [OpenCode](https://opencode.ai) that enables **vision support** for models that lack native image attachment support. Originally built for [MiniMax](https://www.minimax.io/) models, it can be configured to work with any model that requires MCP-based image handling. It restores a simple "paste and ask" workflow by automatically handling image assets and routing them through the [MiniMax Coding Plan MCP](https://github.com/MiniMax-AI/MiniMax-Coding-Plan-MCP).
4
4
 
5
5
  ## Demo
6
6
 
@@ -14,28 +14,28 @@ https://github.com/user-attachments/assets/826f90ea-913f-427e-ace8-0b711302c497
14
14
 
15
15
  When using MiniMax models (for example, MiniMax M2.1) inside OpenCode, users run into a limitation: **vision is not supported via native image attachments**.
16
16
 
17
- MiniMax models rely on the MiniMax Coding Plan MCPs `understand_image` tool, which requires an explicit file path or URL. This breaks the normal chat workflow:
17
+ MiniMax models rely on the MiniMax Coding Plan MCP's `understand_image` tool, which requires an explicit file path or URL. This breaks the normal chat workflow:
18
18
 
19
19
  * **Ignored images**: Images pasted directly into chat are ignored by MiniMax models.
20
20
  * **Manual steps**: Users must save screenshots, locate file paths, and reference them manually.
21
- * **Broken flow**: The paste and ask vision workflow available in other models is lost.
21
+ * **Broken flow**: The "paste and ask" vision workflow available in other models is lost.
22
22
 
23
23
  ## What This Plugin Does
24
24
 
25
- This plugin removes that friction by automating the vision pipeline for MiniMax models.
25
+ This plugin removes that friction by automating the vision pipeline for configured models.
26
26
 
27
27
  Internally, it:
28
28
 
29
- 1. Detects when a MiniMax model is active
29
+ 1. Detects when a configured model is active (MiniMax by default)
30
30
  2. Intercepts images pasted into the chat
31
31
  3. Saves them to a temporary local directory
32
32
  4. Injects the required context so the model can invoke the `understand_image` MCP tool with the correct file path
33
33
 
34
- From the users perspective, pasted images simply work with MiniMax vision.
34
+ From the user's perspective, pasted images simply work with vision, just like how it works out of the box with other vision-capable models like Claude.
35
35
 
36
36
  ## Supported Models
37
37
 
38
- The plugin activates only for MiniMax models, identified by:
38
+ By default, the plugin activates for MiniMax models, identified by:
39
39
 
40
40
  * **Provider ID** containing `minimax`
41
41
  * **Model ID** containing `minimax` or `abab`
@@ -45,7 +45,75 @@ Examples:
45
45
  * `minimax/minimax-m2.1`
46
46
  * `minimax/abab6.5s-chat`
47
47
 
48
- Non-MiniMax models are not affected. Their native vision support continues to work normally.
48
+ ### Custom Model Configuration
49
+
50
+ You can configure which models the plugin applies to by creating a config file.
51
+
52
+ #### Config File Locations
53
+
54
+ The plugin looks for configuration in these locations (in order of priority):
55
+
56
+ 1. **Project level**: `.opencode/opencode-minimax-easy-vision.json`
57
+ 2. **User level**: `~/.config/opencode/opencode-minimax-easy-vision.json`
58
+
59
+ Project-level config takes precedence over user-level config.
60
+
61
+ #### Config File Format
62
+
63
+ ```json
64
+ {
65
+ "models": ["minimax/*", "glm/*", "openai/gpt-4-vision"]
66
+ }
67
+ ```
68
+
69
+ #### Pattern Syntax
70
+
71
+ Model patterns use a `provider/model` format with wildcard support:
72
+
73
+ | Pattern | Description |
74
+ | -------------- | --------------------------------------------------- |
75
+ | `*` | Match ALL models (global wildcard) |
76
+ | `minimax/*` | Match all models from the `minimax` provider |
77
+ | `*/glm-4v` | Match `glm-4v` model from any provider |
78
+ | `openai/gpt-4` | Exact match for provider and model |
79
+ | `*/abab*` | Match any model containing `abab` from any provider |
80
+
81
+ #### Wildcard Rules
82
+
83
+ * `*` at the start matches any prefix: `*suffix` matches values ending with `suffix`
84
+ * `*` at the end matches any suffix: `prefix*` matches values starting with `prefix`
85
+ * `*` alone matches everything
86
+ * `*text*` matches values containing `text`
87
+
88
+ #### Precedence
89
+
90
+ When multiple patterns are specified, the first matching pattern wins. If the `models` array is empty or the config file doesn't exist, the plugin falls back to default MiniMax-only behavior.
91
+
92
+ #### Examples
93
+
94
+ **Enable for all models:**
95
+
96
+ ```json
97
+ {
98
+ "models": ["*"]
99
+ }
100
+ ```
101
+
102
+ **Enable for specific providers:**
103
+
104
+ ```json
105
+ {
106
+ "models": ["minimax/*", "glm/*", "zhipu/*"]
107
+ }
108
+ ```
109
+
110
+ **Mix of providers and specific models:**
111
+
112
+ ```json
113
+ {
114
+ "models": ["minimax/*", "openai/gpt-4-vision", "*/claude-3*"]
115
+ }
116
+ ```
49
117
 
50
118
  ## Supported Image Formats
51
119
 
@@ -53,7 +121,7 @@ Non-MiniMax models are not affected. Their native vision support continues to wo
53
121
  * JPEG
54
122
  * WebP
55
123
 
56
- These formats match the constraints of the MiniMax Coding Plan MCP `understand_image` tool.
124
+ *(These formats are dictated by the limitations of the [MiniMax Coding Plan MCP](https://github.com/MiniMax-AI/MiniMax-Coding-Plan-MCP) `understand_image` tool.)*
57
125
 
58
126
  ## Installation
59
127
 
@@ -105,7 +173,7 @@ For full setup details, refer to the MiniMax Coding Plan MCP and MiniMax API doc
105
173
 
106
174
  ## Usage
107
175
 
108
- 1. Start OpenCode with a supported MiniMax model
176
+ 1. Start OpenCode with a supported model (MiniMax by default, or any configured model)
109
177
  2. Paste an image into the chat (`Cmd+V` / `Ctrl+V`)
110
178
  3. Ask a question about the image
111
179
 
@@ -130,14 +198,6 @@ Model: I'll analyze the screenshot using the understand_image tool.
130
198
  Model: The error message indicates a "TypeError: Cannot read property 'foo' of undefined"...
131
199
  ```
132
200
 
133
- ## Limitations
134
-
135
- * Uses `experimental.chat.messages.transform`, which may change in future OpenCode versions
136
- * Images persist until the OS clears the temporary directory
137
- * Only JPEG, PNG, and WebP are supported
138
- * The MCP server must have access to the local filesystem
139
- * Animated GIFs and unsupported formats are skipped
140
-
141
201
  ## Development
142
202
 
143
203
  ```bash
@@ -149,12 +209,12 @@ The built plugin will be available at `dist/index.js`.
149
209
 
150
210
  ## License
151
211
 
152
- GPL-3.0. See `LICENSE.md` for details.
212
+ GPL-3.0. See [LICENSE.md](./LICENCE.md) for details.
153
213
 
154
214
  ## References
155
215
 
156
- * [https://opencode.ai](https://opencode.ai)
157
- * [https://opencode.ai/docs/plugins/](https://opencode.ai/docs/plugins/)
158
- * [https://www.minimax.io/](https://www.minimax.io/)
159
- * [https://github.com/MiniMax-AI/MiniMax-Coding-Plan-MCP](https://github.com/MiniMax-AI/MiniMax-Coding-Plan-MCP)
160
- * [https://platform.minimax.io/docs/guides/coding-plan-mcp-guide](https://platform.minimax.io/docs/guides/coding-plan-mcp-guide)
216
+ * [OpenCode Official Website](https://opencode.ai)
217
+ * [OpenCode Plugins Documentation](https://opencode.ai/docs/plugins/)
218
+ * [MiniMax Official Website](https://www.minimax.io/)
219
+ * [MiniMax Coding Plan MCP Repository](https://github.com/MiniMax-AI/MiniMax-Coding-Plan-MCP)
220
+ * [MiniMax API Documentation](https://platform.minimax.io/docs/guides/coding-plan-mcp-guide)
package/dist/index.d.ts CHANGED
@@ -1,7 +1,4 @@
1
1
  import type { Plugin } from "@opencode-ai/plugin";
2
- /**
3
- * The main plugin export
4
- */
5
2
  export declare const MinimaxEasyVisionPlugin: Plugin;
6
3
  export default MinimaxEasyVisionPlugin;
7
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAsNlD;;GAEG;AACH,eAAO,MAAM,uBAAuB,EAAE,MAgHrC,CAAC;AAGF,eAAe,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAgTlD,eAAO,MAAM,uBAAuB,EAAE,MAwGrC,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
package/dist/index.js CHANGED
@@ -1,64 +1,137 @@
1
- import { tmpdir } from "node:os";
1
+ import { tmpdir, homedir } from "node:os";
2
2
  import { join } from "node:path";
3
- import { mkdir, writeFile } from "node:fs/promises";
3
+ import { mkdir, writeFile, readFile } from "node:fs/promises";
4
+ import { existsSync } from "node:fs";
4
5
  import { randomUUID } from "node:crypto";
5
- /**
6
- * Plugin name for logging
7
- */
8
6
  const PLUGIN_NAME = "minimax-easy-vision";
9
- /**
10
- * Temp directory name for saved images
11
- */
7
+ const CONFIG_FILENAME = "opencode-minimax-easy-vision.json";
12
8
  const TEMP_DIR_NAME = "opencode-minimax-vision";
13
- /**
14
- * Supported image MIME types (Minimax MCP limitation)
15
- */
16
9
  const SUPPORTED_MIME_TYPES = new Set([
17
10
  "image/png",
18
11
  "image/jpeg",
19
12
  "image/jpg",
20
13
  "image/webp",
21
14
  ]);
22
- /**
23
- * Map MIME type to file extension
24
- */
25
15
  const MIME_TO_EXTENSION = {
26
16
  "image/png": "png",
27
17
  "image/jpeg": "jpg",
28
18
  "image/jpg": "jpg",
29
19
  "image/webp": "webp",
30
20
  };
31
- /**
32
- * Check if a model is a Minimax model
33
- */
34
- function isMinimaxModel(model) {
21
+ const DEFAULT_MODEL_PATTERNS = ["minimax/*", "*/abab*"];
22
+ let pluginConfig = {};
23
+ function getUserConfigPath() {
24
+ return join(homedir(), ".config", "opencode", CONFIG_FILENAME);
25
+ }
26
+ function getProjectConfigPath(directory) {
27
+ return join(directory, ".opencode", CONFIG_FILENAME);
28
+ }
29
+ async function loadConfigFile(configPath) {
30
+ try {
31
+ if (!existsSync(configPath)) {
32
+ return null;
33
+ }
34
+ const content = await readFile(configPath, "utf-8");
35
+ const parsed = JSON.parse(content);
36
+ if (parsed && typeof parsed === "object" && parsed !== null) {
37
+ const config = parsed;
38
+ if (Array.isArray(config.models)) {
39
+ const models = config.models.filter((m) => typeof m === "string");
40
+ return { models };
41
+ }
42
+ }
43
+ return {};
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ }
49
+ // Config precedence: project > user > defaults
50
+ async function loadPluginConfig(directory, log) {
51
+ const userConfigPath = getUserConfigPath();
52
+ const projectConfigPath = getProjectConfigPath(directory);
53
+ const userConfig = await loadConfigFile(userConfigPath);
54
+ const projectConfig = await loadConfigFile(projectConfigPath);
55
+ if (projectConfig?.models && projectConfig.models.length > 0) {
56
+ pluginConfig = projectConfig;
57
+ log(`Loaded project config from ${projectConfigPath}: ${projectConfig.models.join(", ")}`);
58
+ }
59
+ else if (userConfig?.models && userConfig.models.length > 0) {
60
+ pluginConfig = userConfig;
61
+ log(`Loaded user config from ${userConfigPath}: ${userConfig.models.join(", ")}`);
62
+ }
63
+ else {
64
+ pluginConfig = {};
65
+ log(`No config found, using defaults: ${DEFAULT_MODEL_PATTERNS.join(", ")}`);
66
+ }
67
+ }
68
+ // Order matters: check *text* before *text or text* to avoid false matches
69
+ function matchesPattern(pattern, value) {
70
+ const lowerPattern = pattern.toLowerCase();
71
+ const lowerValue = value.toLowerCase();
72
+ if (lowerPattern === "*") {
73
+ return true;
74
+ }
75
+ if (lowerPattern.startsWith("*") &&
76
+ lowerPattern.endsWith("*") &&
77
+ lowerPattern.length > 2) {
78
+ const middle = lowerPattern.slice(1, -1);
79
+ return lowerValue.includes(middle);
80
+ }
81
+ if (lowerPattern.endsWith("*")) {
82
+ const prefix = lowerPattern.slice(0, -1);
83
+ return lowerValue.startsWith(prefix);
84
+ }
85
+ if (lowerPattern.startsWith("*")) {
86
+ const suffix = lowerPattern.slice(1);
87
+ return lowerValue.endsWith(suffix);
88
+ }
89
+ return lowerValue === lowerPattern;
90
+ }
91
+ // Pattern format: "provider/model" with wildcards. No slash = match against both.
92
+ function modelMatchesPatterns(model, patterns) {
35
93
  if (!model)
36
94
  return false;
37
- const providerID = model.providerID.toLowerCase();
38
- const modelID = model.modelID.toLowerCase();
39
- return (providerID.includes("minimax") ||
40
- modelID.includes("minimax") ||
41
- modelID.includes("abab") // Minimax model naming convention
42
- );
95
+ for (const pattern of patterns) {
96
+ if (pattern === "*") {
97
+ return true;
98
+ }
99
+ const slashIndex = pattern.indexOf("/");
100
+ if (slashIndex === -1) {
101
+ if (matchesPattern(pattern, model.modelID)) {
102
+ return true;
103
+ }
104
+ if (matchesPattern(pattern, model.providerID)) {
105
+ return true;
106
+ }
107
+ }
108
+ else {
109
+ const providerPattern = pattern.slice(0, slashIndex);
110
+ const modelPattern = pattern.slice(slashIndex + 1);
111
+ const providerMatches = matchesPattern(providerPattern, model.providerID);
112
+ const modelMatches = matchesPattern(modelPattern, model.modelID);
113
+ if (providerMatches && modelMatches) {
114
+ return true;
115
+ }
116
+ }
117
+ }
118
+ return false;
119
+ }
120
+ function shouldApplyVisionHook(model) {
121
+ const patterns = pluginConfig.models && pluginConfig.models.length > 0
122
+ ? pluginConfig.models
123
+ : DEFAULT_MODEL_PATTERNS;
124
+ return modelMatchesPatterns(model, patterns);
43
125
  }
44
- /**
45
- * Check if a part is a FilePart with an image
46
- */
47
126
  function isImageFilePart(part) {
48
127
  if (part.type !== "file")
49
128
  return false;
50
129
  const filePart = part;
51
130
  return SUPPORTED_MIME_TYPES.has(filePart.mime?.toLowerCase() ?? "");
52
131
  }
53
- /**
54
- * Check if a part is a TextPart
55
- */
56
132
  function isTextPart(part) {
57
133
  return part.type === "text";
58
134
  }
59
- /**
60
- * Parse a data URL and extract the base64 data
61
- */
62
135
  function parseDataUrl(dataUrl) {
63
136
  const match = dataUrl.match(/^data:([^;]+);base64,(.+)$/);
64
137
  if (!match)
@@ -73,23 +146,14 @@ function parseDataUrl(dataUrl) {
73
146
  return null;
74
147
  }
75
148
  }
76
- /**
77
- * Get file extension from MIME type
78
- */
79
149
  function getExtension(mime) {
80
150
  return MIME_TO_EXTENSION[mime.toLowerCase()] ?? "png";
81
151
  }
82
- /**
83
- * Ensure temp directory exists and return its path
84
- */
85
152
  async function ensureTempDir() {
86
153
  const dir = join(tmpdir(), TEMP_DIR_NAME);
87
154
  await mkdir(dir, { recursive: true });
88
155
  return dir;
89
156
  }
90
- /**
91
- * Save image data to a temp file and return the path
92
- */
93
157
  async function saveImageToTemp(data, mime) {
94
158
  const tempDir = await ensureTempDir();
95
159
  const ext = getExtension(mime);
@@ -98,9 +162,6 @@ async function saveImageToTemp(data, mime) {
98
162
  await writeFile(filepath, data);
99
163
  return filepath;
100
164
  }
101
- /**
102
- * Generate the injection prompt for the model
103
- */
104
165
  function generateInjectionPrompt(imagePaths, userText) {
105
166
  if (imagePaths.length === 0)
106
167
  return userText;
@@ -115,10 +176,6 @@ Use the \`mcp_minimax_understand_image\` tool to analyze ${isSingle ? "this imag
115
176
 
116
177
  User's request: ${userText || "(analyze the image)"}`;
117
178
  }
118
- /**
119
- * Process a message and extract/save any images
120
- * Returns the paths of saved images
121
- */
122
179
  async function processMessageImages(parts, log) {
123
180
  const savedImages = [];
124
181
  for (const part of parts) {
@@ -126,12 +183,10 @@ async function processMessageImages(parts, log) {
126
183
  continue;
127
184
  const filePart = part;
128
185
  const url = filePart.url;
129
- // Skip if no URL
130
186
  if (!url) {
131
187
  log(`Skipping image part ${filePart.id}: no URL`);
132
188
  continue;
133
189
  }
134
- // Handle file:// URLs - already on disk
135
190
  if (url.startsWith("file://")) {
136
191
  const localPath = url.replace("file://", "");
137
192
  log(`Image already on disk: ${localPath}`);
@@ -142,7 +197,6 @@ async function processMessageImages(parts, log) {
142
197
  });
143
198
  continue;
144
199
  }
145
- // Handle data: URLs - need to save to disk
146
200
  if (url.startsWith("data:")) {
147
201
  const parsed = parseDataUrl(url);
148
202
  if (!parsed) {
@@ -163,7 +217,6 @@ async function processMessageImages(parts, log) {
163
217
  }
164
218
  continue;
165
219
  }
166
- // Handle HTTP/HTTPS URLs - Minimax can use these directly
167
220
  if (url.startsWith("http://") || url.startsWith("https://")) {
168
221
  log(`Image is remote URL: ${url}`);
169
222
  savedImages.push({
@@ -177,12 +230,8 @@ async function processMessageImages(parts, log) {
177
230
  }
178
231
  return savedImages;
179
232
  }
180
- /**
181
- * The main plugin export
182
- */
183
233
  export const MinimaxEasyVisionPlugin = async (input) => {
184
- const { client } = input;
185
- // Simple logging helper
234
+ const { client, directory } = input;
186
235
  const log = (msg) => {
187
236
  client.app
188
237
  .log({
@@ -192,19 +241,13 @@ export const MinimaxEasyVisionPlugin = async (input) => {
192
241
  message: msg,
193
242
  },
194
243
  })
195
- .catch(() => {
196
- // Ignore logging errors
197
- });
244
+ .catch(() => { });
198
245
  };
246
+ await loadPluginConfig(directory, log);
199
247
  log("Plugin initialized");
200
248
  return {
201
- /**
202
- * Transform messages before they're sent to the LLM
203
- * This is where we intercept images and inject the MCP tool instructions
204
- */
205
249
  "experimental.chat.messages.transform": async (_input, output) => {
206
250
  const { messages } = output;
207
- // Find the last user message
208
251
  let lastUserMessage;
209
252
  let lastUserIndex = -1;
210
253
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -215,21 +258,18 @@ export const MinimaxEasyVisionPlugin = async (input) => {
215
258
  }
216
259
  }
217
260
  if (!lastUserMessage) {
218
- return; // No user message to process
261
+ return;
219
262
  }
220
- // Check if using Minimax model
221
263
  const userInfo = lastUserMessage.info;
222
- if (!isMinimaxModel(userInfo.model)) {
223
- return; // Not a Minimax model, skip
264
+ if (!shouldApplyVisionHook(userInfo.model)) {
265
+ return;
224
266
  }
225
- log("Detected Minimax model, checking for images...");
226
- // Check if there are any image parts
267
+ log("Model matched, checking for images...");
227
268
  const hasImages = lastUserMessage.parts.some(isImageFilePart);
228
269
  if (!hasImages) {
229
- return; // No images to process
270
+ return;
230
271
  }
231
272
  log("Found images in message, processing...");
232
- // Process and save images
233
273
  const savedImages = await processMessageImages(lastUserMessage.parts, log);
234
274
  if (savedImages.length === 0) {
235
275
  log("No images were successfully saved");
@@ -262,5 +302,4 @@ export const MinimaxEasyVisionPlugin = async (input) => {
262
302
  },
263
303
  };
264
304
  };
265
- // Default export for OpenCode plugin loading
266
305
  export default MinimaxEasyVisionPlugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-minimax-easy-vision",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "OpenCode plugin that enables vision support for Minimax models by saving pasted images and injecting MCP tool instructions",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",