codex-copilot-dx 0.4.18 → 0.4.20
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 +11 -3
- package/bin/cli.mjs +7 -0
- package/package.json +1 -5
- package/src/anthropic.mjs +14 -3
- package/src/copilot.mjs +164 -11
- package/src/log.mjs +85 -0
- package/src/status.mjs +1 -0
package/README.md
CHANGED
|
@@ -89,8 +89,12 @@ Environment variables:
|
|
|
89
89
|
| `CCDX_MAX_BODY_BYTES` | `134217728` | Maximum compressed/raw request body size |
|
|
90
90
|
| `CCDX_MAX_DECODED_BODY_BYTES` | `268435456` | Maximum decoded request body size after decompression |
|
|
91
91
|
| `CCDX_UPSTREAM_TIMEOUT_MS` | `120000` | Timeout for non-streaming upstream Copilot requests |
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
92
|
+
| `CCDX_UPSTREAM_RETRIES` | `2` | Retries for transient Copilot upstream network errors; capped at `5` |
|
|
93
|
+
| `CCDX_UPSTREAM_RETRY_DELAY_MS` | `300` | Initial upstream retry backoff in milliseconds; capped at `5000` |
|
|
94
|
+
| `CCDX_LOG_PATH` | unset | Mirror terminal logs to a file; set to `1` for `~/.local/share/codex-copilot-dx/debug.log` |
|
|
95
|
+
| `CCDX_LOG_LEVEL` | `info` | Set to `debug` to include upstream request attempts, status codes, retry causes, and timings |
|
|
96
|
+
| `CCDX_IMG_MAX_DIM` | `2048` | Max long edge in pixels for optional image downscaling |
|
|
97
|
+
| `CCDX_IMG_QUALITY` | `85` | WebP quality used when optional image re-encoding is available |
|
|
94
98
|
| `CCDX_IMG_MIN_BYTES` | `100000` | Images smaller than this are left untouched |
|
|
95
99
|
| `CCDX_IMG_CONCURRENCY` | `4` | Concurrent image optimization tasks; values above `12` are capped at `12` |
|
|
96
100
|
| `CCDX_DISABLE_IMG_OPT` | unset | Set to `1` to disable image optimization |
|
|
@@ -111,6 +115,10 @@ Environment variables:
|
|
|
111
115
|
|
|
112
116
|
The adapter records token usage metadata to `~/.local/share/codex-copilot-dx/usage.jsonl` when upstream responses include usage fields. It logs counts, model names, API surface, and response IDs only; it does not log prompts, completions, tool arguments, or image content.
|
|
113
117
|
|
|
118
|
+
### Debug logging
|
|
119
|
+
|
|
120
|
+
Set `CCDX_LOG_PATH=1` to mirror terminal logs to `~/.local/share/codex-copilot-dx/debug.log`, or set `CCDX_LOG_PATH` to a custom file. Add `CCDX_LOG_LEVEL=debug` to include upstream request attempts, retry causes, status codes, and timings. Debug logs do not include prompts, completions, request bodies, or authorization tokens.
|
|
121
|
+
|
|
114
122
|
```bash
|
|
115
123
|
codex-copilot-dx usage
|
|
116
124
|
```
|
|
@@ -119,7 +127,7 @@ codex-copilot-dx usage
|
|
|
119
127
|
|
|
120
128
|
Long computer-use sessions can accumulate screenshots inside the conversation history. Each screenshot is shipped on later turns, and GitHub Copilot's `/responses` endpoint can reject oversized requests with `413 Payload Too Large`.
|
|
121
129
|
|
|
122
|
-
|
|
130
|
+
When the optional `sharp` package is available, the adapter downsamples embedded screenshots to long-edge <= 2048 px and re-encodes them as WebP before forwarding `/v1/responses`. `sharp` is not installed by default so the CLI stays lightweight and avoids native package install stalls; install it alongside `codex-copilot-dx` if you want this optimization.
|
|
123
131
|
|
|
124
132
|
## License
|
|
125
133
|
|
package/bin/cli.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { startAdapter } from "../src/adapter.mjs";
|
|
|
8
8
|
import { listModels, refreshVSCodeVersion } from "../src/copilot.mjs";
|
|
9
9
|
import { claudeDesktopModelDefsFromCopilotModels, claudeDesktopModelIds, gptModelIdsFromCopilotModels, parseModelAliasEnv } from "../src/models.mjs";
|
|
10
10
|
import { status } from "../src/status.mjs";
|
|
11
|
+
import { configureLogging } from "../src/log.mjs";
|
|
11
12
|
import { printUsageSummary } from "../src/usage.mjs";
|
|
12
13
|
import { checkForUpdate, localPackageVersion } from "../src/version.mjs";
|
|
13
14
|
import { runDoctor } from "../src/doctor.mjs";
|
|
@@ -21,6 +22,12 @@ const EXISTING_ADAPTER_TIMEOUT_MS = parseInt(process.env.CCDX_EXISTING_ADAPTER_T
|
|
|
21
22
|
const LOCAL_VERSION = localPackageVersion();
|
|
22
23
|
const command = process.argv[2];
|
|
23
24
|
const CONFIGURE_CLAUDE_DESKTOP = command === "--configure-claude-desktop" || process.env.CCDX_CONFIGURE_CLAUDE_DESKTOP === "1";
|
|
25
|
+
const LOGGING = configureLogging();
|
|
26
|
+
|
|
27
|
+
if (LOGGING.filePath) {
|
|
28
|
+
console.log(status("info", `Debug log: ${LOGGING.filePath}`));
|
|
29
|
+
if (LOGGING.level === "debug") console.log(status("debug", "Debug logging enabled"));
|
|
30
|
+
}
|
|
24
31
|
|
|
25
32
|
async function refreshClaudeDesktopModelDefs() {
|
|
26
33
|
if (parseModelAliasEnv(process.env.CCDX_CLAUDE_MODEL_ALIASES).length) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-copilot-dx",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.20",
|
|
4
4
|
"description": "Use Codex Desktop, Claude Code, and Claude App with GitHub Copilot",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,9 +27,5 @@
|
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"scripts": {
|
|
29
29
|
"test": "node --test"
|
|
30
|
-
},
|
|
31
|
-
"dependencies": {
|
|
32
|
-
"gpt-tokenizer": "^3.4.0",
|
|
33
|
-
"sharp": "^0.34.5"
|
|
34
30
|
}
|
|
35
31
|
}
|
package/src/anthropic.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { encode } from "gpt-tokenizer";
|
|
3
2
|
|
|
4
3
|
// Anthropic Messages API to OpenAI chat/completions translation.
|
|
5
4
|
// Pure functions only; network calls stay in adapter.mjs and copilot.mjs.
|
|
@@ -238,7 +237,19 @@ export async function streamAnthropicFromLines(lineIterator, emit, model, option
|
|
|
238
237
|
await emit("message_stop", { type: "message_stop" });
|
|
239
238
|
}
|
|
240
239
|
|
|
241
|
-
|
|
240
|
+
function estimateTokens(text) {
|
|
241
|
+
if (!text) return 0;
|
|
242
|
+
let asciiChars = 0;
|
|
243
|
+
let nonAsciiChars = 0;
|
|
244
|
+
for (const ch of text) {
|
|
245
|
+
if (/\s/.test(ch)) continue;
|
|
246
|
+
if (ch.codePointAt(0) <= 0x7f) asciiChars += 1;
|
|
247
|
+
else nonAsciiChars += 1;
|
|
248
|
+
}
|
|
249
|
+
return Math.max(1, Math.ceil(asciiChars / 4) + nonAsciiChars);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Copilot has no count_tokens endpoint; estimate locally without install-time dependencies.
|
|
242
253
|
export function countTokens(body) {
|
|
243
254
|
const parts = [];
|
|
244
255
|
const sys = systemToText(body.system);
|
|
@@ -263,5 +274,5 @@ export function countTokens(body) {
|
|
|
263
274
|
}
|
|
264
275
|
|
|
265
276
|
const text = parts.join("\n");
|
|
266
|
-
return { input_tokens:
|
|
277
|
+
return { input_tokens: estimateTokens(text) };
|
|
267
278
|
}
|
package/src/copilot.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { status } from "./status.mjs";
|
|
5
|
+
import { debugLog } from "./log.mjs";
|
|
5
6
|
import { githubReauthMessage, githubTokenPath, importDiscoveredGithubToken } from "./auth.mjs";
|
|
6
7
|
|
|
7
8
|
export const DEFAULT_API_BASE = "https://api.githubcopilot.com";
|
|
@@ -11,15 +12,33 @@ const IMG_QUALITY = parseInt(process.env.CCDX_IMG_QUALITY || "85", 10);
|
|
|
11
12
|
const IMG_MIN_BYTES = parseInt(process.env.CCDX_IMG_MIN_BYTES || "100000", 10);
|
|
12
13
|
const DEFAULT_IMG_CONCURRENCY = 4;
|
|
13
14
|
const IMG_MAX_CONCURRENCY = 12;
|
|
15
|
+
const DEFAULT_UPSTREAM_RETRIES = 2;
|
|
16
|
+
const MAX_UPSTREAM_RETRIES = 5;
|
|
17
|
+
const DEFAULT_UPSTREAM_RETRY_DELAY_MS = 300;
|
|
18
|
+
const MAX_UPSTREAM_RETRY_DELAY_MS = 5000;
|
|
14
19
|
const IMG_OPT_DISABLED = process.env.CCDX_DISABLE_IMG_OPT === "1";
|
|
15
20
|
const IMG_CONCURRENCY = parseImageConcurrency(process.env.CCDX_IMG_CONCURRENCY);
|
|
21
|
+
const UPSTREAM_RETRIES = parseUpstreamRetries(process.env.CCDX_UPSTREAM_RETRIES);
|
|
22
|
+
const UPSTREAM_RETRY_DELAY_MS = parseUpstreamRetryDelayMs(process.env.CCDX_UPSTREAM_RETRY_DELAY_MS);
|
|
16
23
|
let sharpImport = null;
|
|
24
|
+
let sharpUnavailable = false;
|
|
25
|
+
let sharpUnavailableLogged = false;
|
|
17
26
|
|
|
18
27
|
export function parseImageConcurrency(value) {
|
|
19
28
|
const n = Number.parseInt(value, 10);
|
|
20
29
|
return Number.isFinite(n) && n > 0 ? Math.min(n, IMG_MAX_CONCURRENCY) : DEFAULT_IMG_CONCURRENCY;
|
|
21
30
|
}
|
|
22
31
|
|
|
32
|
+
export function parseUpstreamRetries(value) {
|
|
33
|
+
const n = Number.parseInt(value, 10);
|
|
34
|
+
return Number.isFinite(n) && n >= 0 ? Math.min(n, MAX_UPSTREAM_RETRIES) : DEFAULT_UPSTREAM_RETRIES;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function parseUpstreamRetryDelayMs(value) {
|
|
38
|
+
const n = Number.parseInt(value, 10);
|
|
39
|
+
return Number.isFinite(n) && n > 0 ? Math.min(n, MAX_UPSTREAM_RETRY_DELAY_MS) : DEFAULT_UPSTREAM_RETRY_DELAY_MS;
|
|
40
|
+
}
|
|
41
|
+
|
|
23
42
|
export function parseApiBase(data) {
|
|
24
43
|
return (data && data.endpoints && typeof data.endpoints.api === "string" && data.endpoints.api)
|
|
25
44
|
? data.endpoints.api
|
|
@@ -34,11 +53,138 @@ export function responsesEndpointPath() {
|
|
|
34
53
|
return "/responses";
|
|
35
54
|
}
|
|
36
55
|
const GITHUB_API = "https://api.github.com";
|
|
56
|
+
const RETRYABLE_UPSTREAM_ERROR_CODES = new Set([
|
|
57
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
58
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
59
|
+
"UND_ERR_BODY_TIMEOUT",
|
|
60
|
+
"ETIMEDOUT",
|
|
61
|
+
"ECONNRESET",
|
|
62
|
+
"ECONNREFUSED",
|
|
63
|
+
"EPIPE",
|
|
64
|
+
"EAI_AGAIN",
|
|
65
|
+
"ENOTFOUND",
|
|
66
|
+
]);
|
|
67
|
+
const RETRYABLE_UPSTREAM_STATUSES = new Set([408, 502, 503, 504]);
|
|
68
|
+
|
|
69
|
+
function upstreamTarget(url) {
|
|
70
|
+
try {
|
|
71
|
+
const parsed = new URL(url);
|
|
72
|
+
return `${parsed.hostname}${parsed.pathname}`;
|
|
73
|
+
} catch {
|
|
74
|
+
return String(url);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function upstreamErrorCode(err) {
|
|
79
|
+
return err?.cause?.code || err?.code || "";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isAbortError(err, signal) {
|
|
83
|
+
return signal?.aborted || err?.name === "AbortError" || err?.code === "ABORT_ERR";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function abortError(signal) {
|
|
87
|
+
if (signal?.reason instanceof Error) return signal.reason;
|
|
88
|
+
const err = new Error("The operation was aborted");
|
|
89
|
+
err.name = "AbortError";
|
|
90
|
+
return err;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function sleep(ms, { signal } = {}) {
|
|
94
|
+
if (signal?.aborted) return Promise.reject(abortError(signal));
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
let timer;
|
|
97
|
+
const cleanup = () => {
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
signal?.removeEventListener("abort", onAbort);
|
|
100
|
+
};
|
|
101
|
+
const onAbort = () => {
|
|
102
|
+
cleanup();
|
|
103
|
+
reject(abortError(signal));
|
|
104
|
+
};
|
|
105
|
+
timer = setTimeout(() => {
|
|
106
|
+
cleanup();
|
|
107
|
+
resolve();
|
|
108
|
+
}, ms);
|
|
109
|
+
if (!signal) return;
|
|
110
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function requestMethod(init = {}) {
|
|
115
|
+
return String(init.method || "GET").toUpperCase();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function isRetryableUpstreamError(err, { signal } = {}) {
|
|
119
|
+
if (isAbortError(err, signal)) return false;
|
|
120
|
+
return RETRYABLE_UPSTREAM_ERROR_CODES.has(upstreamErrorCode(err));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function isRetryableUpstreamStatus(status, method = "GET") {
|
|
124
|
+
const safeMethod = ["GET", "HEAD", "OPTIONS"].includes(String(method || "GET").toUpperCase());
|
|
125
|
+
return safeMethod && RETRYABLE_UPSTREAM_STATUSES.has(status);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function upstreamRetryDelay(attempt, baseDelayMs) {
|
|
129
|
+
return Math.min(baseDelayMs * (2 ** attempt), MAX_UPSTREAM_RETRY_DELAY_MS);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function describeUpstreamError(err) {
|
|
133
|
+
const code = upstreamErrorCode(err);
|
|
134
|
+
return [code, err?.cause?.message || err?.message].filter(Boolean).join(": ") || "network error";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function fetchCopilotUpstream(
|
|
138
|
+
url,
|
|
139
|
+
init = {},
|
|
140
|
+
{
|
|
141
|
+
fetchImpl = fetch,
|
|
142
|
+
retries = UPSTREAM_RETRIES,
|
|
143
|
+
retryDelayMs = UPSTREAM_RETRY_DELAY_MS,
|
|
144
|
+
} = {},
|
|
145
|
+
) {
|
|
146
|
+
const retryCount = parseUpstreamRetries(retries);
|
|
147
|
+
const baseDelay = parseUpstreamRetryDelayMs(retryDelayMs);
|
|
148
|
+
const method = requestMethod(init);
|
|
149
|
+
const signal = init.signal;
|
|
150
|
+
const target = upstreamTarget(url);
|
|
151
|
+
const totalStart = Date.now();
|
|
152
|
+
|
|
153
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
154
|
+
const attemptStart = Date.now();
|
|
155
|
+
debugLog(`upstream ${method} ${target} attempt=${attempt + 1}/${retryCount + 1}`);
|
|
156
|
+
try {
|
|
157
|
+
const resp = await fetchImpl(url, init);
|
|
158
|
+
debugLog(`upstream ${method} ${target} status=${resp.status} attempt=${attempt + 1}/${retryCount + 1} attempt_ms=${Date.now() - attemptStart} total_ms=${Date.now() - totalStart}`);
|
|
159
|
+
if (attempt < retryCount && isRetryableUpstreamStatus(resp.status, method)) {
|
|
160
|
+
await resp.arrayBuffer?.().catch(() => {});
|
|
161
|
+
const delay = upstreamRetryDelay(attempt, baseDelay);
|
|
162
|
+
console.warn(status("warn", `upstream ${target} returned ${resp.status}; retry ${attempt + 1}/${retryCount} in ${delay}ms`));
|
|
163
|
+
await sleep(delay, { signal });
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
return resp;
|
|
167
|
+
} catch (e) {
|
|
168
|
+
debugLog(`upstream ${method} ${target} error=${describeUpstreamError(e)} attempt=${attempt + 1}/${retryCount + 1} attempt_ms=${Date.now() - attemptStart} total_ms=${Date.now() - totalStart}`);
|
|
169
|
+
if (attempt >= retryCount || !isRetryableUpstreamError(e, { signal })) throw e;
|
|
170
|
+
const delay = upstreamRetryDelay(attempt, baseDelay);
|
|
171
|
+
console.warn(status("warn", `upstream ${target} ${describeUpstreamError(e)}; retry ${attempt + 1}/${retryCount} in ${delay}ms`));
|
|
172
|
+
await sleep(delay, { signal });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
37
176
|
|
|
38
177
|
async function sharp() {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
178
|
+
if (sharpUnavailable) return null;
|
|
179
|
+
try {
|
|
180
|
+
sharpImport ||= import("sharp");
|
|
181
|
+
const mod = await sharpImport;
|
|
182
|
+
return mod.default || mod;
|
|
183
|
+
} catch {
|
|
184
|
+
sharpUnavailable = true;
|
|
185
|
+
sharpImport = null;
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
42
188
|
}
|
|
43
189
|
|
|
44
190
|
export async function optimizeImageDataUrl(dataUrl) {
|
|
@@ -52,6 +198,13 @@ export async function optimizeImageDataUrl(dataUrl) {
|
|
|
52
198
|
|
|
53
199
|
try {
|
|
54
200
|
const resize = await sharp();
|
|
201
|
+
if (!resize) {
|
|
202
|
+
if (!sharpUnavailableLogged) {
|
|
203
|
+
sharpUnavailableLogged = true;
|
|
204
|
+
console.warn(status("warn", "image optimization skipped because optional dependency sharp is not installed"));
|
|
205
|
+
}
|
|
206
|
+
return dataUrl;
|
|
207
|
+
}
|
|
55
208
|
const out = await resize(raw, { failOn: "none" })
|
|
56
209
|
.rotate()
|
|
57
210
|
.resize(IMG_MAX_DIM, IMG_MAX_DIM, { fit: "inside", withoutEnlargement: true })
|
|
@@ -326,7 +479,7 @@ export function resetCopilotTokenForTests() {
|
|
|
326
479
|
}
|
|
327
480
|
|
|
328
481
|
// chatReq is already converted by the adapter. The caller parses the raw Response.
|
|
329
|
-
export async function chatCompletions(chatReq, { signal } = {}) {
|
|
482
|
+
export async function chatCompletions(chatReq, { signal, fetchImpl, retryOptions } = {}) {
|
|
330
483
|
const token = await getCopilotToken({ signal });
|
|
331
484
|
const messages = chatReq.messages || [];
|
|
332
485
|
const headers = buildHeaders({
|
|
@@ -335,25 +488,25 @@ export async function chatCompletions(chatReq, { signal } = {}) {
|
|
|
335
488
|
initiator: computeInitiator(messages),
|
|
336
489
|
vision: computeVision(messages),
|
|
337
490
|
});
|
|
338
|
-
return
|
|
491
|
+
return fetchCopilotUpstream(`${getApiBase()}/chat/completions`, {
|
|
339
492
|
method: "POST",
|
|
340
493
|
headers,
|
|
341
494
|
body: JSON.stringify(chatReq),
|
|
342
495
|
signal,
|
|
343
|
-
});
|
|
496
|
+
}, { fetchImpl, ...retryOptions });
|
|
344
497
|
}
|
|
345
498
|
|
|
346
|
-
export async function listModels({ signal } = {}) {
|
|
499
|
+
export async function listModels({ signal, fetchImpl, retryOptions } = {}) {
|
|
347
500
|
const token = await getCopilotToken({ signal });
|
|
348
501
|
const headers = buildHeaders({
|
|
349
502
|
token, version: getVSCodeVersion(), initiator: "user", vision: false,
|
|
350
503
|
});
|
|
351
|
-
const resp = await
|
|
504
|
+
const resp = await fetchCopilotUpstream(`${getApiBase()}/models`, { headers, signal }, { fetchImpl, ...retryOptions });
|
|
352
505
|
return { status: resp.status, body: await resp.text() };
|
|
353
506
|
}
|
|
354
507
|
|
|
355
508
|
// Responses-only models go directly to Copilot's /responses endpoint.
|
|
356
|
-
export async function responses(reqBody, { signal } = {}) {
|
|
509
|
+
export async function responses(reqBody, { signal, fetchImpl, retryOptions } = {}) {
|
|
357
510
|
const token = await getCopilotToken({ signal });
|
|
358
511
|
const beforeBytes = Buffer.byteLength(JSON.stringify(reqBody));
|
|
359
512
|
await optimizeImagesInBody(reqBody);
|
|
@@ -371,12 +524,12 @@ export async function responses(reqBody, { signal } = {}) {
|
|
|
371
524
|
headers["Accept"] = reqBody.stream ? "text/event-stream" : "application/json";
|
|
372
525
|
|
|
373
526
|
try {
|
|
374
|
-
return await
|
|
527
|
+
return await fetchCopilotUpstream(`${getApiBase()}${responsesEndpointPath()}`, {
|
|
375
528
|
method: "POST",
|
|
376
529
|
headers,
|
|
377
530
|
body: bodyBuf,
|
|
378
531
|
signal,
|
|
379
|
-
});
|
|
532
|
+
}, { fetchImpl, ...retryOptions });
|
|
380
533
|
} catch (e) {
|
|
381
534
|
const cause = e?.cause;
|
|
382
535
|
const causeText = cause ? ` (${[cause.code, cause.message].filter(Boolean).join(": ")})` : "";
|
package/src/log.mjs
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { format } from "node:util";
|
|
5
|
+
import { status } from "./status.mjs";
|
|
6
|
+
|
|
7
|
+
let globalInstall = null;
|
|
8
|
+
|
|
9
|
+
export function normalizeLogLevel(value) {
|
|
10
|
+
return String(value || "info").trim().toLowerCase() === "debug" ? "debug" : "info";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function defaultLogPath({ home = os.homedir() } = {}) {
|
|
14
|
+
return path.join(home, ".local", "share", "codex-copilot-dx", "debug.log");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function resolveLogPath(value, { home = os.homedir() } = {}) {
|
|
18
|
+
const raw = String(value || "").trim();
|
|
19
|
+
if (!raw) return null;
|
|
20
|
+
if (["1", "true", "yes"].includes(raw.toLowerCase())) return defaultLogPath({ home });
|
|
21
|
+
if (raw === "~") return home;
|
|
22
|
+
if (raw.startsWith("~/")) return path.join(home, raw.slice(2));
|
|
23
|
+
return raw;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isDebugLoggingEnabled(env = process.env) {
|
|
27
|
+
return normalizeLogLevel(env.CCDX_LOG_LEVEL) === "debug";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function renderLogPayload(args) {
|
|
31
|
+
const stamp = new Date().toISOString();
|
|
32
|
+
const text = format(...args);
|
|
33
|
+
return `${text.split(/\r?\n/).map((line) => `${stamp} ${line}`).join("\n")}\n`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function configureLogging({
|
|
37
|
+
env = process.env,
|
|
38
|
+
consoleObj = console,
|
|
39
|
+
home = os.homedir(),
|
|
40
|
+
} = {}) {
|
|
41
|
+
const level = normalizeLogLevel(env.CCDX_LOG_LEVEL);
|
|
42
|
+
const filePath = resolveLogPath(env.CCDX_LOG_PATH, { home });
|
|
43
|
+
if (!filePath) return { level, filePath: null, cleanup: () => {} };
|
|
44
|
+
if (consoleObj === console && globalInstall) {
|
|
45
|
+
return { level, filePath: globalInstall.filePath, cleanup: globalInstall.cleanup };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
49
|
+
|
|
50
|
+
const methods = ["log", "warn", "error", "debug"];
|
|
51
|
+
const originals = Object.fromEntries(methods.map((method) => [
|
|
52
|
+
method,
|
|
53
|
+
typeof consoleObj[method] === "function" ? consoleObj[method].bind(consoleObj) : () => {},
|
|
54
|
+
]));
|
|
55
|
+
let writeFailed = false;
|
|
56
|
+
|
|
57
|
+
function append(args) {
|
|
58
|
+
try {
|
|
59
|
+
fs.appendFileSync(filePath, renderLogPayload(args), { encoding: "utf8", mode: 0o600 });
|
|
60
|
+
} catch (e) {
|
|
61
|
+
if (writeFailed) return;
|
|
62
|
+
writeFailed = true;
|
|
63
|
+
originals.error(`codex-copilot-dx log write failed: ${e.message}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const method of methods) {
|
|
68
|
+
consoleObj[method] = (...args) => {
|
|
69
|
+
originals[method](...args);
|
|
70
|
+
append(args);
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const cleanup = () => {
|
|
75
|
+
for (const method of methods) consoleObj[method] = originals[method];
|
|
76
|
+
if (consoleObj === console && globalInstall?.cleanup === cleanup) globalInstall = null;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
if (consoleObj === console) globalInstall = { filePath, cleanup };
|
|
80
|
+
return { level, filePath, cleanup };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function debugLog(message, env = process.env) {
|
|
84
|
+
if (isDebugLoggingEnabled(env)) console.log(status("debug", message));
|
|
85
|
+
}
|