codex-copilot-dx 0.9.10 → 0.9.11

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
@@ -394,6 +394,10 @@ The summary covers the current log and its single rotated backup, not an unbound
394
394
 
395
395
  Usage writes are asynchronous and batched under the existing cross-process rotation lock. Pending and in-flight records share a 4096-record / 4 MiB ceiling. If either ceiling is reached, new excess records are dropped with a warning and a status counter; inference does not wait or retry. Normal records and per-record rotation boundaries are unchanged. The bounded shutdown flush remains 1.5 seconds; disk failures and shutdown deadlines can prevent persistence, so usage is diagnostic rather than a billing ledger.
396
396
 
397
+ Repeated plain usage summaries in the same process reuse one bounded cache entry when both retained log files are unchanged. File identity, size, modification/change timestamps and a two-second recent-write guard control reuse; concurrent settled-file queries share one scan. The serialized summary budget is 256 KiB and at most 128 model groups; larger results remain complete but uncached. Warnings, unstable reads, metadata errors and scans with changed files are not retained. Each caller receives an independent result. Analytics record callbacks and custom-warning reads keep the original streaming path. There is no persistent index, background polling, cross-process cache or change to usage writes; opening analytics still scans the retained records.
398
+
399
+ Diagnostic redaction removes Basic authorization payloads and URL userinfo, and consumes escaped quotes inside sensitive JSON fields. Ordinary Basic-related prose, safe host/path information and error codes are retained. These protections apply to existing failure/status/debug diagnostics, not forwarded requests or upstream response bodies. They do not make arbitrary upstream prose guaranteed safe to share.
400
+
397
401
  ## Debug logging
398
402
 
399
403
  Set `CCDX_LOG_PATH=1` to mirror terminal logs to `~/.local/share/codex-copilot-dx/debug.log`, or provide a custom path. Add `CCDX_LOG_LEVEL=debug` for upstream attempts, retry causes, status codes, and timings. The adapter does not deliberately dump request/response bodies or authorization headers. File diagnostics redact common credential forms and labelled content echoes, but this cannot identify every secret in arbitrary external prose; review logs before sharing them. File writes are asynchronous and batched with a 4096-record / 2 MiB pending-and-in-flight ceiling. Overflow drops excess file records with one warning and a status counter; terminal output continues. CLI exit paths flush for at most 1.5 seconds. `ccdx doctor config` bypasses file logging entirely.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.9.10",
3
+ "version": "0.9.11",
4
4
  "description": "Use Codex App with GitHub Copilot",
5
5
  "author": "Dale Xiao",
6
6
  "type": "module",
@@ -2,9 +2,16 @@
2
2
  // but remove common credential forms and explicitly labelled content echoes.
3
3
  export function redactDiagnosticText(value) {
4
4
  return String(value ?? "")
5
- .replace(/\bBearer\s+[^\s,;"'<>]+/gi, "Bearer [redacted]")
5
+ .replace(/(\b(?:proxy[-_]?)?authorization["']?\s*[:=]\s*)(?:Basic|Bearer)\s+[^\s,;"'<>\\]+/gi, "$1[redacted]")
6
+ .replace(/\b(Bearer|Basic)\s+([^\s,;"'<>\\]+)/gi, (match, scheme, value) => {
7
+ // Standalone ordinary prose such as "basic authentication" is not a credential.
8
+ if (scheme.toLowerCase() === "basic"
9
+ && (!/^[a-z0-9+/_-]+={0,2}$/i.test(value) || !Buffer.from(value, "base64").includes(0x3a))) return match;
10
+ return `${scheme} [redacted]`;
11
+ })
12
+ .replace(/\b([a-z][a-z0-9+.-]{0,63}:\/\/)[^\s/?#"'<>]*@/gi, "$1[redacted]@")
6
13
  .replace(/\b(?:github_pat_|gh[pousr]_|sk-|copilot_)[A-Za-z0-9._-]+/gi, "[redacted]")
7
- .replace(/((?:api[_-]?key|access[_-]?token|refresh[_-]?token|authorization|cookie)["']?\s*[:=]\s*)(?:"[^"\n]*"|'[^'\n]*'|[^\s,;]+)/gi, "$1[redacted]")
14
+ .replace(/((?:api[_-]?key|access[_-]?token|refresh[_-]?token|authorization|cookie)["']?\s*[:=]\s*)(?:"(?:\\.|[^"\\\n])*"|'(?:\\.|[^'\\\n])*'|[^\s,;]+)/gi, "$1[redacted]")
8
15
  .replace(/((?:prompt|input|completion|tool_arguments|arguments|content)["']?\s*[:=]\s*)[^\n]+/gi, "$1[redacted]")
9
16
  .replace(/(encrypted(?: function output)? content\s+)[A-Za-z0-9+/_=-]{24,}/gi, "$1[redacted]")
10
17
  .replace(/\b[A-Za-z0-9+/_=-]{96,}\b/g, "[redacted]");
package/src/usage.mjs CHANGED
@@ -288,7 +288,7 @@ export function summarizeUsage(records) {
288
288
  return summary;
289
289
  }
290
290
 
291
- export async function summarizeUsageLogs(filePath = usageLogPath(), { warn = console.error, onRecord } = {}) {
291
+ async function summarizeUsageLogFiles(filePath, { warn = console.error, onRecord } = {}) {
292
292
  const summary = { requests: 0, totals: {}, byModel: {} };
293
293
  for (const candidate of [rotatedFilePath(filePath), filePath]) {
294
294
  for await (const record of iterateUsageRecords(candidate, { warn })) {
@@ -305,6 +305,51 @@ export async function summarizeUsageLogs(filePath = usageLogPath(), { warn = con
305
305
  return summary;
306
306
  }
307
307
 
308
+ const SUMMARY_CACHE_MAX_BYTES = 256 * 1024;
309
+ const SUMMARY_CACHE_SETTLED_MS = 2000;
310
+ let cachedSummary = null;
311
+
312
+ async function usageSummaryVersion(filePath) {
313
+ try {
314
+ const files = await Promise.all([rotatedFilePath(filePath), filePath].map(async (file) => {
315
+ try {
316
+ const stat = await fs.promises.stat(file, { bigint: true });
317
+ if (!stat.isFile()) return null;
318
+ const recent = Math.max(Number(stat.mtimeMs), Number(stat.ctimeMs));
319
+ if (Date.now() - recent < SUMMARY_CACHE_SETTLED_MS) return null;
320
+ return [stat.dev, stat.ino, stat.size, stat.mtimeNs, stat.ctimeNs].join(":");
321
+ } catch (error) {
322
+ if (error?.code === "ENOENT") return "missing";
323
+ throw error;
324
+ }
325
+ }));
326
+ return files.some((version) => version === null) ? null : `${path.resolve(filePath)}\0${files.join("|")}`;
327
+ } catch { return null; } // Uncacheable metadata never replaces the normal read/error path.
328
+ }
329
+
330
+ export async function summarizeUsageLogs(filePath = usageLogPath(), { warn = console.error, onRecord } = {}) {
331
+ if (onRecord || warn !== console.error) return summarizeUsageLogFiles(filePath, { warn, onRecord });
332
+ const version = await usageSummaryVersion(filePath);
333
+ if (version === null) {
334
+ cachedSummary = null;
335
+ return summarizeUsageLogFiles(filePath, { warn });
336
+ }
337
+ if (cachedSummary?.version === version) return structuredClone(await cachedSummary.promise);
338
+ const entry = { version, promise: null };
339
+ cachedSummary = entry;
340
+ entry.promise = (async () => {
341
+ let warned = false;
342
+ const summary = await summarizeUsageLogFiles(filePath, { warn(message) { warned = true; warn(message); } });
343
+ const after = await usageSummaryVersion(filePath);
344
+ const retain = !warned && after === version && Object.keys(summary.byModel).length <= 128
345
+ && Buffer.byteLength(JSON.stringify(summary)) <= SUMMARY_CACHE_MAX_BYTES;
346
+ if (!retain && cachedSummary === entry) cachedSummary = null;
347
+ return summary;
348
+ })();
349
+ try { return structuredClone(await entry.promise); }
350
+ catch (error) { if (cachedSummary === entry) cachedSummary = null; throw error; }
351
+ }
352
+
308
353
  function fmt(n) {
309
354
  return Number.isFinite(n) ? n.toLocaleString("en-US") : "0";
310
355
  }