codex-copilot-dx 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/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.
@@ -228,7 +230,7 @@ If a configured npm mirror has not synchronized the current release yet, use the
228
230
 
229
231
  After starting `ccdx`, open [http://127.0.0.1:2026/](http://127.0.0.1:2026/) in a browser. If `ADAPTER_PORT` is customized, use that port instead. The dashboard shares the existing adapter listener; CCDX does not open a browser automatically or start another service. The page and its assets require both a loopback connection and a loopback Host header, even when LAN binding is enabled.
230
232
 
231
- The dashboard loads local status, a live GitHub Copilot model directory, and a summary of local usage metadata once when opened. Each section has an explicit Refresh control; nothing polls in the background. The appearance button switches between the default dark terminal theme (sun icon) and a light theme (moon icon); the choice is stored only in that browser. The live model table uses the same fresh account validation and selectable GPT filtering as `ccdx models`, never labels cached fallback data as online, and makes no inference request. A catalog listing does not guarantee inference access. The Usage table combines the current and rotated local usage logs, shows totals and up to 100 model rows, and contains no prompt, response body, or credential fields. The animation selector previews the existing themes and saves through the same settings writer as `ccdx animation`; changes take effect on the next adapter start, and `CCDX_TERMINAL_ANIMATION=off` still disables terminal motion. Dashboard data requests require a custom header that cross-site simple requests cannot send; animation writes also require a matching same-origin `Origin`. The dashboard does not initialize the optional image handler. The overview's known-model count is the adapter's current in-memory catalog, not the live table. A missing image-generation status does not prove the feature is disabled: use `ccdx image-status` to check setup. Recent upstream error prose may contain sensitive details, so review the page before sharing screenshots.
233
+ The dashboard loads local status, a live GitHub Copilot model directory, and a summary of local usage metadata once when opened. Each section has an explicit Refresh control; nothing polls in the background. The appearance button switches between the default dark terminal theme (sun icon) and a light theme (moon icon); the choice is stored only in that browser. The Requests card shows the running adapter's active raw and decoded per-request body limits, not request-body usage; Local History shows its in-memory Responses history use and active limit. Limit changes remain CLI-only (`ccdx limits` and `ccdx cache`). The live model table uses the same fresh account validation and selectable GPT filtering as `ccdx models`, never labels cached fallback data as online, and makes no inference request. A catalog listing does not guarantee inference access. The Usage table combines the current and rotated local usage logs, shows totals and up to 100 model rows, and contains no prompt, response body, or credential fields. The animation selector previews the existing themes and saves through the same settings writer as `ccdx animation`; changes take effect on the next adapter start, and `CCDX_TERMINAL_ANIMATION=off` still disables terminal motion. Dashboard data requests require a custom header that cross-site simple requests cannot send; animation writes also require a matching same-origin `Origin`. The dashboard does not initialize the optional image handler. The overview's known-model count is the adapter's current in-memory catalog, not the live table. A missing image-generation status does not prove the feature is disabled: use `ccdx image-status` to check setup. Recent upstream error prose may contain sensitive details, so review the page before sharing screenshots.
232
234
 
233
235
  The transparent, single-color double-peak favicon follows the dashboard's saved light/dark choice without a separate network request.
234
236
 
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.2",
4
4
  "description": "Use Codex App with GitHub Copilot",
5
5
  "author": "Dale Xiao",
6
6
  "type": "module",
@@ -27,7 +27,7 @@
27
27
  <article class="card"><h2>ADAPTER</h2><strong id="version">—</strong><p id="uptime">—</p></article>
28
28
  <article class="card"><h2>COPILOT</h2><strong id="copilot">—</strong><p id="token">—</p></article>
29
29
  <article class="card"><h2>KNOWN MODELS</h2><strong id="models">—</strong><p id="model-source">—</p></article>
30
- <article class="card"><h2>REQUESTS</h2><strong id="requests">—</strong><p id="request-errors">—</p></article>
30
+ <article class="card"><h2>REQUESTS</h2><strong id="requests">—</strong><p id="request-errors">—</p><p>Per-request limits (active)<br><span id="body-limits">—</span></p></article>
31
31
  </section>
32
32
 
33
33
  <section class="details" aria-label="Runtime details">
@@ -37,7 +37,7 @@
37
37
  <p class="hint">HTTP 2xx != model completed.</p>
38
38
  </article>
39
39
  <article class="panel">
40
- <div class="panel-heading"><h2>LOCAL HISTORY</h2><span>MEMORY</span></div>
40
+ <div class="panel-heading"><h2>LOCAL HISTORY</h2><span>ACTIVE LIMIT</span></div>
41
41
  <strong id="history">—</strong><progress id="history-meter" class="meter" max="100" value="0" aria-label="History cache usage"></progress>
42
42
  <p id="history-detail" class="hint">—</p>
43
43
  </article>
@@ -50,6 +50,7 @@ function render(data) {
50
50
  const requests = data.requests || {};
51
51
  text("requests", number(requests.total));
52
52
  text("request-errors", `4xx ${number(requests.status_4xx)} / 5xx ${number(requests.status_5xx)} / active ${number(requests.active)}`);
53
+ text("body-limits", `raw ${mib(data.limits?.max_body_bytes)} / decoded ${mib(data.limits?.max_decoded_body_bytes)}`);
53
54
 
54
55
  const outcomes = data.stream_performance?.by_route?.responses?.terminal_outcomes?.totals || {};
55
56
  for (const key of ["completed", "incomplete", "failed", "cancelled"]) text(key, number(outcomes[key]));
@@ -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);