opencode-see-image 0.9.1 → 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 +7 -1
- package/README.md +1 -1
- package/index.ts +61 -21
- package/package.json +2 -2
|
@@ -6,7 +6,13 @@
|
|
|
6
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
7
|
"Bash(echo \"--- created, exit $? ---\")",
|
|
8
8
|
"Bash(node -p \"require\\('./package.json'\\).version\")",
|
|
9
|
-
"Bash(echo \"local package.json version: $\\(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 *)"
|
|
10
16
|
]
|
|
11
17
|
}
|
|
12
18
|
}
|
package/README.md
CHANGED
|
@@ -140,7 +140,7 @@ export SEE_IMAGE_MODEL="kimi-k2.7-code"
|
|
|
140
140
|
|
|
141
141
|
## Updating
|
|
142
142
|
|
|
143
|
-
**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.
|
|
144
144
|
|
|
145
145
|
**Manual update** (if you want to force it now):
|
|
146
146
|
```bash
|
package/index.ts
CHANGED
|
@@ -218,7 +218,6 @@ function readProviderKey(providerID: string): string | null {
|
|
|
218
218
|
|
|
219
219
|
async function seeImageViaSDK(
|
|
220
220
|
client: any,
|
|
221
|
-
$: any,
|
|
222
221
|
dataUrl: string,
|
|
223
222
|
mediaType: string,
|
|
224
223
|
prompt: string,
|
|
@@ -226,28 +225,54 @@ async function seeImageViaSDK(
|
|
|
226
225
|
): Promise<{ text: string; model: string; provider: string }> {
|
|
227
226
|
const errors: string[] = []
|
|
228
227
|
|
|
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.
|
|
231
228
|
const b64 = dataUrl.split(",")[1] || ""
|
|
232
229
|
const ext =
|
|
233
230
|
Object.entries(EXT_MEDIA).find(([, m]) => m === mediaType)?.[0] || "png"
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
+
}
|
|
238
247
|
|
|
239
248
|
// For free opencode models, use CLI instead of SDK (SDK returns empty).
|
|
240
|
-
// Bun
|
|
241
|
-
//
|
|
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.
|
|
242
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)
|
|
243
273
|
try {
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
proc.text(),
|
|
247
|
-
new Promise<never>((_, reject) =>
|
|
248
|
-
setTimeout(() => reject(new Error(`timed out after ${TIMEOUT}ms`)), TIMEOUT),
|
|
249
|
-
),
|
|
250
|
-
])
|
|
274
|
+
const out = await new Response(proc.stdout).text()
|
|
275
|
+
await proc.exited
|
|
251
276
|
for (const line of out.split("\n").filter(Boolean)) {
|
|
252
277
|
try {
|
|
253
278
|
const parsed = JSON.parse(line)
|
|
@@ -256,7 +281,10 @@ async function seeImageViaSDK(
|
|
|
256
281
|
}
|
|
257
282
|
} catch {}
|
|
258
283
|
}
|
|
259
|
-
} catch {}
|
|
284
|
+
} catch {} finally {
|
|
285
|
+
clearTimeout(timer)
|
|
286
|
+
abort?.removeEventListener("abort", onAbort)
|
|
287
|
+
}
|
|
260
288
|
return null
|
|
261
289
|
}
|
|
262
290
|
|
|
@@ -286,7 +314,15 @@ async function seeImageViaSDK(
|
|
|
286
314
|
|
|
287
315
|
let sessionID: string | undefined
|
|
288
316
|
try {
|
|
289
|
-
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
|
+
])
|
|
290
326
|
sessionID = sessionRes.data?.id
|
|
291
327
|
if (!sessionID) {
|
|
292
328
|
errors.push(`${providerID}/${modelID}: no session ID`)
|
|
@@ -344,7 +380,10 @@ async function seeImageViaSDK(
|
|
|
344
380
|
|
|
345
381
|
if (!result) {
|
|
346
382
|
const apiKey =
|
|
347
|
-
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")
|
|
348
387
|
if (apiKey) {
|
|
349
388
|
try {
|
|
350
389
|
result = await seeImageViaHTTP(b64, mediaType, prompt, abort, apiKey)
|
|
@@ -364,7 +403,9 @@ async function seeImageViaSDK(
|
|
|
364
403
|
`see_image: SDK vision call failed for all candidates. ${errMsg}.${hint}`,
|
|
365
404
|
)
|
|
366
405
|
} finally {
|
|
367
|
-
|
|
406
|
+
if (tmpPath) {
|
|
407
|
+
try { fs.unlinkSync(tmpPath) } catch {}
|
|
408
|
+
}
|
|
368
409
|
}
|
|
369
410
|
}
|
|
370
411
|
|
|
@@ -500,7 +541,6 @@ const SeeImagePlugin: Plugin = async (ctx) => {
|
|
|
500
541
|
} else {
|
|
501
542
|
result = await seeImageViaSDK(
|
|
502
543
|
client,
|
|
503
|
-
$,
|
|
504
544
|
resolved.dataUrl,
|
|
505
545
|
resolved.mediaType,
|
|
506
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
|
}
|