codex-copilot-dx 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.
package/README.md CHANGED
@@ -184,6 +184,8 @@ The provider credential is stored in `~/.config/codex-copilot-dx/image-provider.
184
184
 
185
185
  The enable command verifies the local MCP handshake and generation-tool directory without generating an image. It reports provider configuration separately from local service readiness; it cannot assert that every running Codex task has refreshed its catalog. Codex normally detects changed skills; restart Codex App once if an open task has not refreshed them. Normal CCDX startup installs or updates this guidance only for an already-enabled provider, without repeating API setup. Without `enable-image`, it installs neither this skill nor the MCP registration. Unchanged guidance is not rewritten. The existing adapter can remain running because provider settings are loaded when the tool is invoked.
186
186
 
187
+ For Qwen-style image responses, CCDX accepts canonical PNG, JPEG, or WebP `data:image/...;base64,` output within the existing 32 MiB image limit. Public HTTPS image URLs still use the same pinned-DNS download checks; HTTP, private-network targets, and URLs with embedded credentials remain rejected.
188
+
187
189
  Existing compatible `ccdx_image` registrations are reused without requiring CCDX's comment markers or resetting user timeouts/tool policies. Missing or damaged markers alone do not block startup. Core GPT configuration is committed independently before optional image maintenance: image configuration conflicts, image-only write failures, or Skill maintenance failures warn without aborting the core service. No new image guidance is installed after failed image configuration. With images disabled or their provider configuration invalid/unreadable, normal startup performs no image configuration maintenance, Skill installation, or image-network probe; explicit `disable-image` owns cleanup. Automatic updates/removal are limited to recognizable CCDX defaults and isolated TOML declarations, never everything between two comments. Ambiguous shared-inline or customized registrations are left intact; `disable-image` still disables the CCDX provider and removes its owned guidance. Invalid TOML/UTF-8 in the shared Codex config or a failed essential core configuration write remains an explicit error rather than a silent success.
188
190
 
189
191
  Direct MCP generations and edits retain their original inline image content and editing ID, and also save the exact image bytes privately under `~/.local/share/codex-copilot-dx/images`. The result includes ready-to-use image Markdown for Codex's final chat reply, so the image appears as a clickable thumbnail instead of only inside a tool result. Files use unique names and are not overwritten or removed on adapter restart. Asking to show the same image again reuses its existing Markdown, not a new generation or edit. If saving fails, CCDX still returns the generated image content, reports the delivery failure, and does not retry the provider or invent a file path. The bundled helper continues saving once at its existing output location, without a second adapter-side copy. Existing enabled users receive updated, unmodified CCDX-owned guidance on the next CCDX startup; no new setup command is needed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Use Codex App with GitHub Copilot",
5
5
  "author": "Dale Xiao",
6
6
  "type": "module",
@@ -73,6 +73,29 @@ function checkedImage(config, value) {
73
73
  return value;
74
74
  }
75
75
 
76
+ function decodeGeneratedImageDataUri(value, signal) {
77
+ if (signal?.aborted) throw providerError("Image generation timed out or was cancelled", "ccdx_image_timeout");
78
+ const maxEncodedLength = 4 * Math.ceil(IMAGE_MAX_BYTES / 3);
79
+ if (value.length > maxEncodedLength + 32) {
80
+ throw providerError("Generated image exceeds the size limit", "ccdx_image_too_large");
81
+ }
82
+ const match = /^data:(image\/(?:png|jpeg|webp));base64,/.exec(value);
83
+ if (!match) throw providerError("Image API returned an unsupported image data URI", "ccdx_image_format_unsupported");
84
+ const encoded = value.slice(match[0].length);
85
+ if (!encoded || encoded.length > maxEncodedLength || encoded.length % 4 !== 0
86
+ || !/^[A-Za-z0-9+/]+={0,2}$/.test(encoded)) {
87
+ throw providerError("Image API returned invalid image data", "ccdx_image_data_invalid");
88
+ }
89
+ const bytes = Buffer.from(encoded, "base64");
90
+ if (bytes.length > IMAGE_MAX_BYTES) throw providerError("Generated image exceeds the size limit", "ccdx_image_too_large");
91
+ if (bytes.toString("base64") !== encoded) throw providerError("Image API returned invalid image data", "ccdx_image_data_invalid");
92
+ if (imageMetadata(bytes).mimeType !== match[1]) {
93
+ throw providerError("Image API returned image data with a mismatched format", "ccdx_image_format_unsupported");
94
+ }
95
+ if (signal?.aborted) throw providerError("Image generation timed out or was cancelled", "ccdx_image_timeout");
96
+ return bytes;
97
+ }
98
+
76
99
  function generationBody(config, prompt, size, image) {
77
100
  if (config.protocol === "qwen-messages") {
78
101
  return {
@@ -306,9 +329,9 @@ export async function generateImage(config, {
306
329
  if (!source) throw providerError("Image API response did not contain an image");
307
330
  let bytes;
308
331
  try {
309
- bytes = source.type === "base64"
310
- ? Buffer.from(source.value, "base64")
311
- : await downloadImage(source.value, { signal: requestSignal, maxBytes: IMAGE_MAX_BYTES });
332
+ if (source.type === "base64") bytes = Buffer.from(source.value, "base64");
333
+ else if (source.value.startsWith("data:")) bytes = decodeGeneratedImageDataUri(source.value, requestSignal);
334
+ else bytes = await downloadImage(source.value, { signal: requestSignal, maxBytes: IMAGE_MAX_BYTES });
312
335
  } catch (error) {
313
336
  if (error?.code?.startsWith?.("ccdx_")) throw error;
314
337
  const code = String(error?.cause?.code || error?.code || "").replace(/[^A-Z0-9_-]/gi, "").slice(0, 64);