opencode-see-image 0.9.0 → 0.9.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.
@@ -0,0 +1,12 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(bun build *)",
5
+ "Bash(npm view *)",
6
+ "Bash(FILTER_BRANCH_SQUELCH_WARNING=1 git filter-branch -f --msg-filter 'sed \"/Co-Authored-By: Claude/d\" | sed -e :a -e \"/^\\\\n*$/{\\\\$d;N;ba\" -e \"}\"' HEAD~2..HEAD)",
7
+ "Bash(echo \"--- created, exit $? ---\")",
8
+ "Bash(node -p \"require\\('./package.json'\\).version\")",
9
+ "Bash(echo \"local package.json version: $\\(node -p \"require\\('./package.json'\\).version\" \\)\")"
10
+ ]
11
+ }
12
+ }
package/README.md CHANGED
@@ -42,7 +42,7 @@ You need a connected vision-capable provider. The plugin auto-detects whichever
42
42
  2. Select **opencode** (OpenCode Zen)
43
43
  3. Paste your API key from [opencode.ai/auth](https://opencode.ai/auth)
44
44
 
45
- The plugin falls back to **big-pickle** (~12000ms). No subscription needed.
45
+ The plugin falls back to **mimo-v2.5-free**. No subscription needed.
46
46
 
47
47
  ### Paid, w/ OpenCode Go
48
48
  1. Run `/connect` in opencode
@@ -55,7 +55,7 @@ The plugin prefers **minimax-m3** via opencode-go (~3000ms) when available.
55
55
 
56
56
  Set the `SEE_IMAGE_*` env vars to point at any Anthropic-Messages-compatible endpoint. See [Configuration](#configuration) below.
57
57
 
58
- **Resolution order:** explicit `SEE_IMAGE_API_KEY` env → configured `SEE_IMAGE_PROVIDER` → `opencode-go` (MiniMax M3) → `opencode` (big-pickle, free).
58
+ **Resolution order:** explicit `SEE_IMAGE_API_KEY` env → configured `SEE_IMAGE_PROVIDER` → `opencode-go` (MiniMax M3) → `opencode` (mimo-v2.5-free, free).
59
59
 
60
60
  ## How it works
61
61
 
@@ -126,7 +126,8 @@ export SEE_IMAGE_MODEL="kimi-k2.7-code"
126
126
 
127
127
  | Model | Speed | Notes |
128
128
  |---|---|---|
129
- | `big-pickle` | ~12000ms | Free. Accurate. Default fallback when only Zen is connected. |
129
+ | `mimo-v2.5-free` | | Free. Default fallback when only Zen is connected (routed via CLI). |
130
+ | `big-pickle` | ~12000ms | Free. Accurate. Alternative Zen fallback. |
130
131
 
131
132
  **Paid (OpenCode Go):**
132
133
 
package/index.ts CHANGED
@@ -11,7 +11,7 @@ const ENDPOINT =
11
11
  "https://opencode.ai/zen/go/v1/messages"
12
12
  const MODEL = process.env.SEE_IMAGE_MODEL || "minimax-m3"
13
13
  const PROVIDER_ID = process.env.SEE_IMAGE_PROVIDER || "opencode-go"
14
- const TIMEOUT = parseInt(process.env.SEE_IMAGE_TIMEOUT || "10000", 10)
14
+ const TIMEOUT = parseInt(process.env.SEE_IMAGE_TIMEOUT || "30000", 10)
15
15
  const API_VERSION = process.env.SEE_IMAGE_API_VERSION || "2023-06-01"
16
16
  const USER_AGENT =
17
17
  process.env.SEE_IMAGE_USER_AGENT ||
@@ -47,8 +47,9 @@ function resolveFromDb(
47
47
  const dbPath = opencodeDbPath()
48
48
  if (!fs.existsSync(dbPath)) return null
49
49
 
50
+ let db: Database | undefined
50
51
  try {
51
- const db = new Database(dbPath, { readonly: true })
52
+ db = new Database(dbPath, { readonly: true })
52
53
  let rows: Array<{ data: string }>
53
54
 
54
55
  if (!filename || filename === "clipboard") {
@@ -98,8 +99,6 @@ function resolveFromDb(
98
99
  }
99
100
  }
100
101
 
101
- db.close()
102
-
103
102
  if (!rows.length) return null
104
103
  const part = JSON.parse(rows[0].data)
105
104
  const url: string = part.url || ""
@@ -112,6 +111,8 @@ function resolveFromDb(
112
111
  }
113
112
  } catch {
114
113
  return null
114
+ } finally {
115
+ db?.close()
115
116
  }
116
117
  }
117
118
 
@@ -225,21 +226,28 @@ async function seeImageViaSDK(
225
226
  ): Promise<{ text: string; model: string; provider: string }> {
226
227
  const errors: string[] = []
227
228
 
228
- // Write image to a temp file so the server can read it directly
229
+ // Write image to a temp file so the server can read it directly. Use the
230
+ // real extension so the CLI can sniff the type correctly.
229
231
  const b64 = dataUrl.split(",")[1] || ""
230
- const tmpPath = path.join(os.tmpdir(), `see-image-${Date.now()}.png`)
232
+ const ext =
233
+ Object.entries(EXT_MEDIA).find(([, m]) => m === mediaType)?.[0] || "png"
234
+ const tmpPath = path.join(os.tmpdir(), `see-image-${Date.now()}.${ext}`)
231
235
  try {
232
236
  fs.writeFileSync(tmpPath, Buffer.from(b64, "base64"))
233
237
  } catch {}
234
238
 
235
- // For free opencode models, use CLI instead of SDK (SDK returns empty)
239
+ // For free opencode models, use CLI instead of SDK (SDK returns empty).
240
+ // Bun's $ doesn't accept an AbortSignal, so race the output against a
241
+ // timeout to actually bound how long a slow model can hang us.
236
242
  const freeFallback = async (modelID: string, userPrompt: string): Promise<string | null> => {
237
243
  try {
238
- const controller = new AbortController()
239
- const timer = setTimeout(() => controller.abort(), TIMEOUT)
240
- const proc = $`opencode run -f ${tmpPath} -m opencode/${modelID} ${userPrompt} --format json --dangerously-skip-permissions`
241
- const out = await proc.text()
242
- clearTimeout(timer)
244
+ const proc = $`opencode run -f ${tmpPath} -m opencode/${modelID} ${userPrompt} --format json --dangerously-skip-permissions`.nothrow()
245
+ const out = await Promise.race([
246
+ proc.text(),
247
+ new Promise<never>((_, reject) =>
248
+ setTimeout(() => reject(new Error(`timed out after ${TIMEOUT}ms`)), TIMEOUT),
249
+ ),
250
+ ])
243
251
  for (const line of out.split("\n").filter(Boolean)) {
244
252
  try {
245
253
  const parsed = JSON.parse(line)
@@ -252,7 +260,6 @@ async function seeImageViaSDK(
252
260
  return null
253
261
  }
254
262
 
255
- const fileUrl = tmpPath
256
263
  let result: { text: string; model: string; provider: string } | undefined
257
264
 
258
265
  try {
@@ -287,22 +294,29 @@ async function seeImageViaSDK(
287
294
  }
288
295
 
289
296
  const controller = new AbortController()
297
+ const onAbort = () => controller.abort()
298
+ abort?.addEventListener("abort", onAbort)
290
299
  const timer = setTimeout(() => controller.abort(), TIMEOUT)
291
- const res = await client.session.prompt({
292
- path: { id: sessionID },
293
- body: {
294
- model: { providerID, modelID },
295
- parts: [
296
- { type: "file", mime: mediaType, url: providerID === "opencode" ? fileUrl : dataUrl },
297
- { type: "text", text: prompt },
298
- ],
299
- tools: {},
300
- system:
301
- "You are a vision assistant. Describe the image accurately and concisely. Answer with text only.",
302
- },
303
- signal: controller.signal,
304
- })
305
- clearTimeout(timer)
300
+ let res
301
+ try {
302
+ res = await client.session.prompt({
303
+ path: { id: sessionID },
304
+ body: {
305
+ model: { providerID, modelID },
306
+ parts: [
307
+ { type: "file", mime: mediaType, url: dataUrl },
308
+ { type: "text", text: prompt },
309
+ ],
310
+ tools: {},
311
+ system:
312
+ "You are a vision assistant. Describe the image accurately and concisely. Answer with text only.",
313
+ },
314
+ signal: controller.signal,
315
+ })
316
+ } finally {
317
+ clearTimeout(timer)
318
+ abort?.removeEventListener("abort", onAbort)
319
+ }
306
320
 
307
321
  const parts = res.data?.parts ?? []
308
322
  const text = (parts as any[])
@@ -344,7 +358,7 @@ async function seeImageViaSDK(
344
358
 
345
359
  const errMsg = errors.join("; ")
346
360
  const hint = errMsg.includes("usage limit")
347
- ? ` Enable usage from your balance at https://opencode.ai/workspace/wrk_01KVARG0A0Y87XV5JYBNJ0WRXB/go`
361
+ ? ` Enable usage from your balance in your opencode workspace at https://opencode.ai/workspace`
348
362
  : ""
349
363
  throw new Error(
350
364
  `see_image: SDK vision call failed for all candidates. ${errMsg}.${hint}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-see-image",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Give non-vision opencode models the ability to see images/screenshots by routing them to a vision-capable model (MiniMax M3 via opencode-go by default).",
5
5
  "type": "module",
6
6
  "main": "index.ts",