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.
@@ -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 every opencode 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
+ **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
- const tmpPath = path.join(os.tmpdir(), `see-image-${Date.now()}.${ext}`)
235
- try {
236
- fs.writeFileSync(tmpPath, Buffer.from(b64, "base64"))
237
- } catch {}
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'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.
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 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
- ])
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 client.session.create({ body: {} })
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 || readProviderKey("opencode-go")
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
- try { fs.unlinkSync(tmpPath) } catch {}
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.1",
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.1.0"
26
+ "opencode-plugin-update-kit": "^0.2.0"
27
27
  }
28
28
  }