codex-copilot-dx 0.8.10 → 0.9.0

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
@@ -131,6 +131,17 @@ ccdx cache --clean --history
131
131
 
132
132
  Plain `--clean` clears only completed image transforms; the next matching request may recompute them. Adding `--history` also invalidates all process-local `previous_response_id` chains and can make a task's next continuation fail, so it requires interactive confirmation or `--yes`. Cleanup waits for active cache users to finish and never deletes Codex transcripts, generated image files, provider credentials, image edit handles, model catalogs, or usage/debug logs. A stopped or older adapter must be restarted before runtime cleanup; a saved limit applies on the next start.
133
133
 
134
+ ### Request-body limits
135
+
136
+ ```bash
137
+ ccdx limits
138
+ ccdx limits --decoded 256
139
+ ccdx limits -256
140
+ ccdx limits --decoded default
141
+ ```
142
+
143
+ `ccdx limits` distinguishes the saved decoded request-body limit from the running adapter's active limit. The default is 128 MiB; the opt-in saved limit accepts 128–512 MiB. Changes take effect only after restarting the adapter, never restart it automatically, and do not clear conversations or caches. `CCDX_MAX_DECODED_BODY_BYTES` overrides the saved setting; unset it before changing the saved limit. The separate 64 MiB raw/compressed-body limit is not changed. Larger decoded requests can use substantially more process memory during JSON parsing and image preparation; raising this limit may enable a large thread to compact, but does not guarantee successful inference or compaction. Return to `default` after temporary use.
144
+
134
145
  ### Terminal animation
135
146
 
136
147
  Choose the terminal activity animation interactively:
@@ -219,6 +230,8 @@ After starting `ccdx`, open [http://127.0.0.1:2026/](http://127.0.0.1:2026/) in
219
230
 
220
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.
221
232
 
233
+ The transparent, single-color double-peak favicon follows the dashboard's saved light/dark choice without a separate network request.
234
+
222
235
  Inspect only the local Codex configuration:
223
236
 
224
237
  ```bash
package/bin/cli.mjs CHANGED
@@ -101,6 +101,24 @@ if (CLI.command === "cache") {
101
101
  process.exit(1);
102
102
  }
103
103
  }
104
+ if (CLI.command === "limits") {
105
+ try {
106
+ const probe = parseAdapterProbeOptions(process.env);
107
+ const { runLimitsCommand } = await import("../src/cli-limits.mjs");
108
+ await runLimitsCommand({
109
+ action: CLI.action,
110
+ limitMib: CLI.limitMib,
111
+ resetLimit: CLI.resetLimit,
112
+ host: probe.adapterHost,
113
+ port: probe.adapterPort,
114
+ timeoutMs: probe.existingAdapterTimeoutMs,
115
+ });
116
+ process.exit(0);
117
+ } catch (e) {
118
+ console.error(status("err", e.message));
119
+ process.exit(1);
120
+ }
121
+ }
104
122
  if (CLI.command === "retired") {
105
123
  console.error(status("err", `${CLI.integration} integration was retired in ccdx 0.7.0.`));
106
124
  if (CLI.integration === "PM Studio") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.8.10",
3
+ "version": "0.9.0",
4
4
  "description": "Use Codex App with GitHub Copilot",
5
5
  "author": "Dale Xiao",
6
6
  "type": "module",
@@ -0,0 +1,55 @@
1
+ import os from "node:os";
2
+ import { MIB } from "./cache-limits.mjs";
3
+ import { readAdapterStatus } from "./cli-status.mjs";
4
+ import { loadRuntimeConfig } from "./runtime-config.mjs";
5
+ import { decodedBodyLimitPreference, writeDecodedBodyLimitMib } from "./user-settings.mjs";
6
+
7
+ function mib(bytes) {
8
+ const value = Number(bytes);
9
+ return `${(value / MIB).toFixed(value % MIB === 0 ? 0 : 1)} MiB`;
10
+ }
11
+
12
+ export async function runLimitsCommand({
13
+ action = "status",
14
+ limitMib,
15
+ resetLimit = false,
16
+ env = process.env,
17
+ home = os.homedir(),
18
+ host = "127.0.0.1",
19
+ port = 2026,
20
+ output = process.stdout,
21
+ fetchImpl = fetch,
22
+ timeoutMs = 1000,
23
+ } = {}) {
24
+ if (action === "set") {
25
+ if (String(env.CCDX_MAX_DECODED_BODY_BYTES || "").trim()) {
26
+ throw new Error("CCDX_MAX_DECODED_BODY_BYTES overrides saved limits; unset it before using ccdx limits --decoded");
27
+ }
28
+ writeDecodedBodyLimitMib(resetLimit ? null : limitMib, { env, home });
29
+ } else if (action !== "status") {
30
+ throw new Error(`Unknown limits action: ${action}`);
31
+ }
32
+
33
+ const preference = decodedBodyLimitPreference({ env, home });
34
+ let runtime;
35
+ try {
36
+ runtime = await readAdapterStatus({ host, port, timeoutMs, fetchImpl });
37
+ } catch {
38
+ runtime = null;
39
+ }
40
+ const activeDecoded = Number(runtime?.data?.limits?.max_decoded_body_bytes);
41
+ const activeRaw = Number(runtime?.data?.limits?.max_body_bytes);
42
+ const lines = [
43
+ "ccdx limits",
44
+ `Configured decoded request body: ${mib(preference.bytes)} (${preference.source})`,
45
+ runtime && Number.isFinite(activeDecoded) && activeDecoded > 0
46
+ ? `Running decoded request body: ${mib(activeDecoded)}`
47
+ : "Running decoded request body: unavailable",
48
+ `Raw request body: ${mib(runtime && Number.isFinite(activeRaw) && activeRaw > 0 ? activeRaw : loadRuntimeConfig(env).maxBodyBytes)}${runtime ? " (running)" : " (configured)"}`,
49
+ ];
50
+ if (!runtime) lines.push("Running adapter status unavailable; restart ccdx to apply and verify this limit.");
51
+ else if (activeDecoded !== preference.bytes) lines.push("Restart ccdx to apply the configured decoded-body limit.");
52
+ if (action === "set") lines.push("Existing conversations and caches were not modified.");
53
+ output.write(`${lines.join("\n")}\n`);
54
+ return { preference, runtime };
55
+ }
@@ -21,6 +21,7 @@ const HELP_TOPICS = new Set([
21
21
  "models",
22
22
  "usage",
23
23
  "cache",
24
+ "limits",
24
25
  "animation",
25
26
  "enable-image",
26
27
  "disable-image",
@@ -187,6 +188,21 @@ export function parseCliArgs(args = []) {
187
188
  yes: Boolean(options["--yes"]),
188
189
  });
189
190
  }
191
+ if (command === "limits") {
192
+ const limitArgs = rest.length === 1 && /^-\d+$/.test(rest[0])
193
+ ? ["--decoded", rest[0].slice(1)]
194
+ : rest;
195
+ const options = parseOptions(limitArgs, new Map([["--decoded", "value"]]));
196
+ const rawLimit = options["--decoded"];
197
+ if (rawLimit && rawLimit !== "default" && !/^\d+$/.test(rawLimit)) {
198
+ throw new Error(`Decoded request-body limit must be an integer MiB value or default: ${rawLimit}`);
199
+ }
200
+ return baseCommand("limits", {
201
+ action: rawLimit ? "set" : "status",
202
+ limitMib: rawLimit && rawLimit !== "default" ? Number(rawLimit) : undefined,
203
+ resetLimit: rawLimit === "default",
204
+ });
205
+ }
190
206
  if (PM_COMMANDS.has(command)) {
191
207
  return baseCommand("retired", { integration: "PM Studio" });
192
208
  }
@@ -334,6 +350,7 @@ function topicHelp(name, topic) {
334
350
  models: `Usage:\n ${name} models [--format table|plain]\n\nPerforms a fresh, read-only Copilot model-directory lookup for the saved account. Interactive terminals use a table by default.`,
335
351
  usage: `Usage:\n ${name} usage [--format table|plain]\n\nSummarizes local token usage metadata without reading prompt or completion content. Interactive terminals use a table by default.`,
336
352
  cache: `Usage:\n ${name} cache\n ${name} cache --limit <MiB|default>\n ${name} cache --clean [--history] [--yes]\n\nShows runtime cache use, saves and applies the shared response-history limit, or clears the rebuildable image-transform cache. --history also clears in-memory response history for all tasks and can make their next continuation fail, so interactive confirmation or --yes is required. Generated image files and saved Codex transcripts are never deleted. Short aliases: -128 sets 128 MiB and -clean equals --clean.`,
353
+ limits: `Usage:\n ${name} limits\n ${name} limits --decoded <MiB|default>\n\nShows configured and running request-body limits, or saves a decoded request-body limit from 128 to 512 MiB. The raw-body limit is unchanged. Restart ccdx to apply a saved change; no conversation or cache is cleared. Short alias: -256 sets 256 MiB.`,
337
354
  animation: `Usage:\n ${name} animation\n\nPreviews all terminal activity animations together and selects the theme used the next time the adapter starts.`,
338
355
  "enable-image": `Usage:\n ${name} enable-image\n\nInteractively configures an HTTPS image API endpoint (base URLs are also accepted) and API key, detects its supported image model and protocol, installs CCDX image guidance, and checks the local image tool for Codex App without generating an image. A blank key preserves the saved key only for the same origin; a new origin requires explicit key entry. Concurrent unrelated config edits are preserved.`,
339
356
  "disable-image": `Usage:\n ${name} disable-image\n\nDisables the local CCDX image tool and removes its stored API credential and unmodified CCDX image guidance.`,
@@ -358,6 +375,7 @@ export function cliHelp(commandName = "ccdx", topic = "") {
358
375
  ${name} models [--format table|plain]
359
376
  ${name} usage [--format table|plain]
360
377
  ${name} cache [--limit <MiB|default> | --clean [--history] [--yes]]
378
+ ${name} limits [--decoded <MiB|default>]
361
379
  ${name} animation
362
380
  ${name} enable-image
363
381
  ${name} disable-image
@@ -375,6 +393,7 @@ Commands:
375
393
  models Query a saved account's live Copilot model catalog
376
394
  usage Summarize locally recorded token usage
377
395
  cache Inspect, size, or clear bounded runtime caches
396
+ limits Inspect or configure the decoded request-body limit
378
397
  animation Select the terminal activity animation
379
398
  enable-image Configure and enable the optional image provider
380
399
  disable-image Disable the image provider and remove its credential
@@ -4,7 +4,7 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <meta name="color-scheme" content="dark light">
7
- <link rel="icon" href="data:,">
7
+ <link id="favicon" rel="icon" type="image/svg+xml" href="data:,">
8
8
  <script src="/theme.js"></script>
9
9
  <link rel="stylesheet" href="/ui.css">
10
10
  <script src="/ui.js" defer></script>
@@ -4,7 +4,13 @@
4
4
  try {
5
5
  if (localStorage.getItem(key) === "light") theme = "light";
6
6
  } catch { /* Private browsing may disable storage. */ }
7
- document.documentElement.dataset.theme = theme;
7
+ const applyTheme = () => {
8
+ document.documentElement.dataset.theme = theme;
9
+ const color = theme === "dark" ? "#a6f5a8" : "#176b3a";
10
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="M7 27v10M12 22v20M17 14v36M22 19v26M27 25v14M32 28v8M37 25v14M42 19v26M47 14v36M52 22v20M57 27v10" fill="none" stroke="${color}" stroke-width="3.5" stroke-linecap="round"/></svg>`;
11
+ document.getElementById("favicon").href = `data:image/svg+xml,${encodeURIComponent(svg)}`;
12
+ };
13
+ applyTheme();
8
14
 
9
15
  document.addEventListener("DOMContentLoaded", () => {
10
16
  const button = document.getElementById("theme-toggle");
@@ -16,7 +22,7 @@
16
22
  updateLabel();
17
23
  button.addEventListener("click", () => {
18
24
  theme = theme === "dark" ? "light" : "dark";
19
- document.documentElement.dataset.theme = theme;
25
+ applyTheme();
20
26
  try { localStorage.setItem(key, theme); } catch { /* Keep the current selection. */ }
21
27
  updateLabel();
22
28
  });
@@ -1,6 +1,7 @@
1
1
  import { promisify } from "node:util";
2
2
  import * as zlib from "node:zlib";
3
3
  import { loadRuntimeConfig, parsePositiveInteger } from "./runtime-config.mjs";
4
+ import { decodedBodyLimitPreference } from "./user-settings.mjs";
4
5
  import { status } from "./status.mjs";
5
6
  import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
6
7
  import { redactDiagnosticText } from "./diagnostic-text.mjs";
@@ -13,7 +14,7 @@ const zstdDecompressAsync = zlib.zstdDecompress ? promisify(zlib.zstdDecompress)
13
14
 
14
15
  const HTTP_RUNTIME_CONFIG = loadRuntimeConfig();
15
16
  const MAX_BODY_BYTES = HTTP_RUNTIME_CONFIG.maxBodyBytes;
16
- const MAX_DECODED_BODY_BYTES = HTTP_RUNTIME_CONFIG.maxDecodedBodyBytes;
17
+ const MAX_DECODED_BODY_BYTES = decodedBodyLimitPreference().bytes;
17
18
  const MAX_INFLIGHT_BODY_BYTES = HTTP_RUNTIME_CONFIG.maxInflightBodyBytes;
18
19
  const MAX_QUEUED_REQUESTS = HTTP_RUNTIME_CONFIG.maxQueuedRequests;
19
20
  const REQUEST_QUEUE_TIMEOUT_MS = HTTP_RUNTIME_CONFIG.requestQueueTimeoutMs;
@@ -4,12 +4,14 @@ import { copilotRuntimeStatus } from "./copilot.mjs";
4
4
  import { imageOptimizationStats } from "./image-optimization.mjs";
5
5
  import { responseHistoryStats } from "./response-history.mjs";
6
6
  import { loadRuntimeConfig } from "./runtime-config.mjs";
7
+ import { decodedBodyLimitPreference } from "./user-settings.mjs";
7
8
  import { profileRouting } from "./profile-routing.mjs";
8
9
  import { usageLoggingStats } from "./usage.mjs";
9
10
  import { debugLoggingStats } from "./log.mjs";
10
11
 
11
12
  export { ADAPTER_STATUS_PATH };
12
13
  const OBSERVABILITY_RUNTIME_CONFIG = loadRuntimeConfig();
14
+ const OBSERVABILITY_DECODED_BODY_BYTES = decodedBodyLimitPreference().bytes;
13
15
 
14
16
  const ROUTE_NAMES = Object.freeze([
15
17
  "responses",
@@ -208,7 +210,7 @@ export function runtimeStatusPayload({
208
210
  routing: profileRouting(),
209
211
  limits: {
210
212
  max_body_bytes: OBSERVABILITY_RUNTIME_CONFIG.maxBodyBytes,
211
- max_decoded_body_bytes: OBSERVABILITY_RUNTIME_CONFIG.maxDecodedBodyBytes,
213
+ max_decoded_body_bytes: OBSERVABILITY_DECODED_BODY_BYTES,
212
214
  max_sse_buffer_bytes: OBSERVABILITY_RUNTIME_CONFIG.maxSseBufferBytes,
213
215
  response_history_max_bytes: history.maxBytes,
214
216
  response_history_max_entries: history.maxEntries,
@@ -3,7 +3,7 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { atomicWriteFileIfChangedSync } from "./atomic-file.mjs";
5
5
  import { DEFAULT_CODEX_AUTO_REVIEW_MODEL } from "./models.mjs";
6
- import { loadRuntimeConfig } from "./runtime-config.mjs";
6
+ import { loadRuntimeConfig, RUNTIME_DEFAULTS } from "./runtime-config.mjs";
7
7
  import {
8
8
  MIB,
9
9
  RESPONSE_HISTORY_DEFAULT_MIB,
@@ -19,6 +19,9 @@ import {
19
19
  const AUTO_REVIEW_MODEL_KEY = "auto_review_model";
20
20
  const TERMINAL_ANIMATION_KEY = "terminal_animation";
21
21
  const RESPONSE_HISTORY_MAX_MIB_KEY = "response_history_max_mib";
22
+ const DECODED_BODY_LIMIT_MIB_KEY = "decoded_body_limit_mib";
23
+ export const DECODED_BODY_LIMIT_MIN_MIB = RUNTIME_DEFAULTS.maxDecodedBodyBytes / MIB;
24
+ export const DECODED_BODY_LIMIT_MAX_MIB = 512;
22
25
  export { RESPONSE_HISTORY_MAX_MIB, RESPONSE_HISTORY_MIN_MIB };
23
26
 
24
27
  export function userSettingsPath({ env = process.env, home = os.homedir() } = {}) {
@@ -66,6 +69,17 @@ export function readUserSettings({ env = process.env, home = os.homedir(), stric
66
69
  delete parsed[RESPONSE_HISTORY_MAX_MIB_KEY];
67
70
  }
68
71
  }
72
+ if (Object.hasOwn(parsed, DECODED_BODY_LIMIT_MIB_KEY)) {
73
+ const value = parsed[DECODED_BODY_LIMIT_MIB_KEY];
74
+ if (!Number.isSafeInteger(value)
75
+ || value < DECODED_BODY_LIMIT_MIN_MIB
76
+ || value > DECODED_BODY_LIMIT_MAX_MIB) {
77
+ if (strict) {
78
+ throw invalidSettings(filePath, `${DECODED_BODY_LIMIT_MIB_KEY} must be an integer from ${DECODED_BODY_LIMIT_MIN_MIB} to ${DECODED_BODY_LIMIT_MAX_MIB}`);
79
+ }
80
+ delete parsed[DECODED_BODY_LIMIT_MIB_KEY];
81
+ }
82
+ }
69
83
  return parsed;
70
84
  } catch (error) {
71
85
  if (error?.code === "ENOENT") return {};
@@ -114,6 +128,39 @@ export function responseHistoryLimitPreference({ env = process.env, home = os.ho
114
128
  return { bytes: RESPONSE_HISTORY_DEFAULT_MIB * MIB, mib: RESPONSE_HISTORY_DEFAULT_MIB, source: "default" };
115
129
  }
116
130
 
131
+ export function decodedBodyLimitPreference({ env = process.env, home = os.homedir() } = {}) {
132
+ const environment = String(env.CCDX_MAX_DECODED_BODY_BYTES || "").trim();
133
+ if (environment) {
134
+ return { bytes: loadRuntimeConfig(env).maxDecodedBodyBytes, source: "environment" };
135
+ }
136
+ const settings = readUserSettings({ env, home });
137
+ const mib = settings[DECODED_BODY_LIMIT_MIB_KEY];
138
+ if (Number.isSafeInteger(mib)) return { bytes: mib * MIB, mib, source: "settings" };
139
+ return { bytes: RUNTIME_DEFAULTS.maxDecodedBodyBytes, mib: DECODED_BODY_LIMIT_MIN_MIB, source: "default" };
140
+ }
141
+
142
+ export function writeDecodedBodyLimitMib(mib, { env = process.env, home = os.homedir() } = {}) {
143
+ const filePath = userSettingsPath({ env, home });
144
+ const settings = readUserSettings({ env, home, strict: true });
145
+ const next = { ...settings };
146
+ if (mib === null || mib === undefined || mib === "") {
147
+ if (!Object.hasOwn(next, DECODED_BODY_LIMIT_MIB_KEY)) {
148
+ return { changed: false, filePath, ...decodedBodyLimitPreference({ env, home }) };
149
+ }
150
+ delete next[DECODED_BODY_LIMIT_MIB_KEY];
151
+ } else {
152
+ const parsed = Number(mib);
153
+ if (!Number.isSafeInteger(parsed)
154
+ || parsed < DECODED_BODY_LIMIT_MIN_MIB
155
+ || parsed > DECODED_BODY_LIMIT_MAX_MIB) {
156
+ throw new Error(`Decoded request-body limit must be an integer from ${DECODED_BODY_LIMIT_MIN_MIB} to ${DECODED_BODY_LIMIT_MAX_MIB} MiB`);
157
+ }
158
+ next[DECODED_BODY_LIMIT_MIB_KEY] = parsed;
159
+ }
160
+ const changed = atomicWriteFileIfChangedSync(filePath, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
161
+ return { changed, filePath, ...decodedBodyLimitPreference({ env, home }) };
162
+ }
163
+
117
164
  export function writeResponseHistoryLimitMib(mib, { env = process.env, home = os.homedir() } = {}) {
118
165
  const filePath = userSettingsPath({ env, home });
119
166
  const settings = readUserSettings({ env, home, strict: true });