codex-copilot-dx 0.8.11 → 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 +13 -0
- package/bin/cli.mjs +18 -0
- package/package.json +1 -1
- package/src/cli-limits.mjs +55 -0
- package/src/cli-options.mjs +19 -0
- package/src/http-transport.mjs +2 -1
- package/src/image-provider.mjs +26 -3
- package/src/observability.mjs +3 -1
- package/src/user-settings.mjs +48 -1
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:
|
|
@@ -173,6 +184,8 @@ The provider credential is stored in `~/.config/codex-copilot-dx/image-provider.
|
|
|
173
184
|
|
|
174
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.
|
|
175
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
|
+
|
|
176
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.
|
|
177
190
|
|
|
178
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/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
|
@@ -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
|
+
}
|
package/src/cli-options.mjs
CHANGED
|
@@ -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
|
package/src/http-transport.mjs
CHANGED
|
@@ -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 =
|
|
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;
|
package/src/image-provider.mjs
CHANGED
|
@@ -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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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);
|
package/src/observability.mjs
CHANGED
|
@@ -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:
|
|
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,
|
package/src/user-settings.mjs
CHANGED
|
@@ -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 });
|