opencode-see-image 0.9.0 → 0.9.2
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/.claude/settings.local.json +18 -0
- package/README.md +5 -4
- package/index.ts +92 -38
- package/package.json +2 -2
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
"Bash(bun --version)",
|
|
11
|
+
"Bash(bun pm *)",
|
|
12
|
+
"Read(//Users/alfa/Documents/opencodeprojects/opencode-see-image/bun-types/**)",
|
|
13
|
+
"Bash(bun run *)",
|
|
14
|
+
"WebFetch(domain:docs.z.ai)",
|
|
15
|
+
"Bash(npm publish *)"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
}
|
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 **
|
|
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` (
|
|
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
|
-
| `
|
|
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
|
|
|
@@ -139,7 +140,7 @@ export SEE_IMAGE_MODEL="kimi-k2.7-code"
|
|
|
139
140
|
|
|
140
141
|
## Updating
|
|
141
142
|
|
|
142
|
-
**Auto-update (built in):** the plugin checks npm for a newer version on
|
|
143
|
+
**Auto-update (built in):** the plugin checks npm for a newer version on startup. If one exists, it updates itself via `opencode plugin --force` (uses opencode's bundled bun, no global bun needed) and shows a toast: *"opencode-see-image updated to X.Y.Z, restart opencode to apply"*. You just need to restart opencode to load the new version. Nothing to configure.
|
|
143
144
|
|
|
144
145
|
**Manual update** (if you want to force it now):
|
|
145
146
|
```bash
|
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 || "
|
|
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
|
-
|
|
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
|
|
|
@@ -217,7 +218,6 @@ function readProviderKey(providerID: string): string | null {
|
|
|
217
218
|
|
|
218
219
|
async function seeImageViaSDK(
|
|
219
220
|
client: any,
|
|
220
|
-
$: any,
|
|
221
221
|
dataUrl: string,
|
|
222
222
|
mediaType: string,
|
|
223
223
|
prompt: string,
|
|
@@ -225,21 +225,54 @@ async function seeImageViaSDK(
|
|
|
225
225
|
): Promise<{ text: string; model: string; provider: string }> {
|
|
226
226
|
const errors: string[] = []
|
|
227
227
|
|
|
228
|
-
// Write image to a temp file so the server can read it directly
|
|
229
228
|
const b64 = dataUrl.split(",")[1] || ""
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
229
|
+
const ext =
|
|
230
|
+
Object.entries(EXT_MEDIA).find(([, m]) => m === mediaType)?.[0] || "png"
|
|
231
|
+
|
|
232
|
+
// The free CLI fallback needs the image on disk. Write it lazily and only
|
|
233
|
+
// once, so the common SDK/dataURL path never touches the filesystem. Use the
|
|
234
|
+
// real extension so the CLI can sniff the type correctly.
|
|
235
|
+
let tmpPath: string | null = null
|
|
236
|
+
const ensureTmpFile = (): string | null => {
|
|
237
|
+
if (tmpPath) return tmpPath
|
|
238
|
+
const p = path.join(os.tmpdir(), `see-image-${Date.now()}.${ext}`)
|
|
239
|
+
try {
|
|
240
|
+
fs.writeFileSync(p, Buffer.from(b64, "base64"))
|
|
241
|
+
tmpPath = p
|
|
242
|
+
} catch {
|
|
243
|
+
return null
|
|
244
|
+
}
|
|
245
|
+
return tmpPath
|
|
246
|
+
}
|
|
234
247
|
|
|
235
|
-
// For free opencode models, use CLI instead of SDK (SDK returns empty)
|
|
248
|
+
// For free opencode models, use CLI instead of SDK (SDK returns empty).
|
|
249
|
+
// Use Bun.spawn (not $) so we get a killable handle: Bun's $ ShellPromise
|
|
250
|
+
// has no .kill(), so racing it against a timeout would leak the process.
|
|
251
|
+
// We kill the child on both timeout and external abort.
|
|
236
252
|
const freeFallback = async (modelID: string, userPrompt: string): Promise<string | null> => {
|
|
253
|
+
const filePath = ensureTmpFile()
|
|
254
|
+
if (!filePath) return null
|
|
255
|
+
const proc = Bun.spawn(
|
|
256
|
+
[
|
|
257
|
+
"opencode",
|
|
258
|
+
"run",
|
|
259
|
+
"-f",
|
|
260
|
+
filePath,
|
|
261
|
+
"-m",
|
|
262
|
+
`opencode/${modelID}`,
|
|
263
|
+
userPrompt,
|
|
264
|
+
"--format",
|
|
265
|
+
"json",
|
|
266
|
+
"--dangerously-skip-permissions",
|
|
267
|
+
],
|
|
268
|
+
{ stdout: "pipe", stderr: "ignore" },
|
|
269
|
+
)
|
|
270
|
+
const timer = setTimeout(() => proc.kill(), TIMEOUT)
|
|
271
|
+
const onAbort = () => proc.kill()
|
|
272
|
+
abort?.addEventListener("abort", onAbort)
|
|
237
273
|
try {
|
|
238
|
-
const
|
|
239
|
-
|
|
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)
|
|
274
|
+
const out = await new Response(proc.stdout).text()
|
|
275
|
+
await proc.exited
|
|
243
276
|
for (const line of out.split("\n").filter(Boolean)) {
|
|
244
277
|
try {
|
|
245
278
|
const parsed = JSON.parse(line)
|
|
@@ -248,11 +281,13 @@ async function seeImageViaSDK(
|
|
|
248
281
|
}
|
|
249
282
|
} catch {}
|
|
250
283
|
}
|
|
251
|
-
} catch {}
|
|
284
|
+
} catch {} finally {
|
|
285
|
+
clearTimeout(timer)
|
|
286
|
+
abort?.removeEventListener("abort", onAbort)
|
|
287
|
+
}
|
|
252
288
|
return null
|
|
253
289
|
}
|
|
254
290
|
|
|
255
|
-
const fileUrl = tmpPath
|
|
256
291
|
let result: { text: string; model: string; provider: string } | undefined
|
|
257
292
|
|
|
258
293
|
try {
|
|
@@ -279,7 +314,15 @@ async function seeImageViaSDK(
|
|
|
279
314
|
|
|
280
315
|
let sessionID: string | undefined
|
|
281
316
|
try {
|
|
282
|
-
const sessionRes = await
|
|
317
|
+
const sessionRes = await Promise.race([
|
|
318
|
+
client.session.create({ body: {} }),
|
|
319
|
+
new Promise<never>((_, reject) =>
|
|
320
|
+
setTimeout(
|
|
321
|
+
() => reject(new Error(`session.create timed out after ${TIMEOUT}ms`)),
|
|
322
|
+
TIMEOUT,
|
|
323
|
+
),
|
|
324
|
+
),
|
|
325
|
+
])
|
|
283
326
|
sessionID = sessionRes.data?.id
|
|
284
327
|
if (!sessionID) {
|
|
285
328
|
errors.push(`${providerID}/${modelID}: no session ID`)
|
|
@@ -287,22 +330,29 @@ async function seeImageViaSDK(
|
|
|
287
330
|
}
|
|
288
331
|
|
|
289
332
|
const controller = new AbortController()
|
|
333
|
+
const onAbort = () => controller.abort()
|
|
334
|
+
abort?.addEventListener("abort", onAbort)
|
|
290
335
|
const timer = setTimeout(() => controller.abort(), TIMEOUT)
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
336
|
+
let res
|
|
337
|
+
try {
|
|
338
|
+
res = await client.session.prompt({
|
|
339
|
+
path: { id: sessionID },
|
|
340
|
+
body: {
|
|
341
|
+
model: { providerID, modelID },
|
|
342
|
+
parts: [
|
|
343
|
+
{ type: "file", mime: mediaType, url: dataUrl },
|
|
344
|
+
{ type: "text", text: prompt },
|
|
345
|
+
],
|
|
346
|
+
tools: {},
|
|
347
|
+
system:
|
|
348
|
+
"You are a vision assistant. Describe the image accurately and concisely. Answer with text only.",
|
|
349
|
+
},
|
|
350
|
+
signal: controller.signal,
|
|
351
|
+
})
|
|
352
|
+
} finally {
|
|
353
|
+
clearTimeout(timer)
|
|
354
|
+
abort?.removeEventListener("abort", onAbort)
|
|
355
|
+
}
|
|
306
356
|
|
|
307
357
|
const parts = res.data?.parts ?? []
|
|
308
358
|
const text = (parts as any[])
|
|
@@ -330,7 +380,10 @@ async function seeImageViaSDK(
|
|
|
330
380
|
|
|
331
381
|
if (!result) {
|
|
332
382
|
const apiKey =
|
|
333
|
-
process.env.SEE_IMAGE_API_KEY ||
|
|
383
|
+
process.env.SEE_IMAGE_API_KEY ||
|
|
384
|
+
(process.env.SEE_IMAGE_PROVIDER &&
|
|
385
|
+
readProviderKey(process.env.SEE_IMAGE_PROVIDER)) ||
|
|
386
|
+
readProviderKey("opencode-go")
|
|
334
387
|
if (apiKey) {
|
|
335
388
|
try {
|
|
336
389
|
result = await seeImageViaHTTP(b64, mediaType, prompt, abort, apiKey)
|
|
@@ -344,13 +397,15 @@ async function seeImageViaSDK(
|
|
|
344
397
|
|
|
345
398
|
const errMsg = errors.join("; ")
|
|
346
399
|
const hint = errMsg.includes("usage limit")
|
|
347
|
-
? ` Enable usage from your balance at https://opencode.ai/workspace
|
|
400
|
+
? ` Enable usage from your balance in your opencode workspace at https://opencode.ai/workspace`
|
|
348
401
|
: ""
|
|
349
402
|
throw new Error(
|
|
350
403
|
`see_image: SDK vision call failed for all candidates. ${errMsg}.${hint}`,
|
|
351
404
|
)
|
|
352
405
|
} finally {
|
|
353
|
-
|
|
406
|
+
if (tmpPath) {
|
|
407
|
+
try { fs.unlinkSync(tmpPath) } catch {}
|
|
408
|
+
}
|
|
354
409
|
}
|
|
355
410
|
}
|
|
356
411
|
|
|
@@ -486,7 +541,6 @@ const SeeImagePlugin: Plugin = async (ctx) => {
|
|
|
486
541
|
} else {
|
|
487
542
|
result = await seeImageViaSDK(
|
|
488
543
|
client,
|
|
489
|
-
$,
|
|
490
544
|
resolved.dataUrl,
|
|
491
545
|
resolved.mediaType,
|
|
492
546
|
prompt,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-see-image",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
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",
|
|
@@ -23,6 +23,6 @@
|
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@opencode-ai/plugin": "^1.15.0",
|
|
26
|
-
"opencode-plugin-update-kit": "^0.
|
|
26
|
+
"opencode-plugin-update-kit": "^0.2.0"
|
|
27
27
|
}
|
|
28
28
|
}
|