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 +85 -25
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +114 -75
- package/package.json +1 -1
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**
|
|
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 MCP
|
|
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
|
|
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
|
|
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
|
|
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 user
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
212
|
+
GPL-3.0. See [LICENSE.md](./LICENCE.md) for details.
|
|
153
213
|
|
|
154
214
|
## References
|
|
155
215
|
|
|
156
|
-
* [
|
|
157
|
-
* [
|
|
158
|
-
* [
|
|
159
|
-
* [
|
|
160
|
-
* [
|
|
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
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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;
|
|
261
|
+
return;
|
|
219
262
|
}
|
|
220
|
-
// Check if using Minimax model
|
|
221
263
|
const userInfo = lastUserMessage.info;
|
|
222
|
-
if (!
|
|
223
|
-
return;
|
|
264
|
+
if (!shouldApplyVisionHook(userInfo.model)) {
|
|
265
|
+
return;
|
|
224
266
|
}
|
|
225
|
-
log("
|
|
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;
|
|
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.
|
|
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",
|