codex-copilot-dx 0.4.44 → 0.5.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
@@ -30,6 +30,8 @@ ccdx
30
30
  The shorter `ccdx` command is the primary launcher. The existing `codex-copilot-dx`
31
31
  command remains an equivalent compatibility alias. For a one-off run without a
32
32
  global install, use `npx codex-copilot-dx@latest`.
33
+ Both installed commands expose the same options and subcommands; help, version,
34
+ doctor, status, and argument-error output use the command name that was invoked.
33
35
 
34
36
  On first run, it will:
35
37
  1. Authenticate with GitHub via device flow (if needed), after first trying compatible local Copilot token sources
@@ -75,7 +77,15 @@ ccdx doctor --compat
75
77
 
76
78
  The compatibility doctor sends a few minimal Copilot requests to check Codex Auto-review, native Responses, streaming history, compaction, image tool namespace handling, and Anthropic streaming. It consumes a small amount of Copilot usage, never starts the adapter or device flow, and does not change client configuration. Combine it with `--online` when both the saved-token entitlement check and the adapter protocol checks are needed.
77
79
 
78
- For live runtime diagnostics, query the running adapter from the same machine:
80
+ For a read-only summary of the running adapter, use:
81
+
82
+ ```bash
83
+ ccdx status
84
+ ```
85
+
86
+ The compatibility alias supports the same command as
87
+ `codex-copilot-dx status`. For the complete machine-readable payload, query the
88
+ loopback endpoint directly:
79
89
 
80
90
  ```bash
81
91
  curl -s http://127.0.0.1:2026/_ccdx/status | jq
package/bin/cli.mjs CHANGED
@@ -16,23 +16,25 @@ import { adapterBaseUrl, checkRunningAdapter } from "../src/running-adapter.mjs"
16
16
  import { assertSafeAdapterHost, isLoopbackHost } from "../src/security.mjs";
17
17
  import { isValidModelList, loadModelCache, saveModelCache } from "../src/model-cache.mjs";
18
18
  import { initializeModelRegistry, runInBackground } from "../src/startup.mjs";
19
- import { cliHelp, parseCliArgs, parseRuntimeOptions } from "../src/cli-options.mjs";
19
+ import { cliCommandName, cliHelp, parseAdapterProbeOptions, parseCliArgs, parseRuntimeOptions } from "../src/cli-options.mjs";
20
+ import { formatAdapterStatus, readAdapterStatus } from "../src/cli-status.mjs";
20
21
  import { closeHttpServer } from "../src/shutdown.mjs";
21
22
 
22
23
  const LOCAL_VERSION = localPackageVersion();
23
- const CLI_BANNER = `codex-copilot-dx v${LOCAL_VERSION} by Dale Xiao`;
24
+ const CLI_NAME = cliCommandName();
25
+ const CLI_BANNER = `${CLI_NAME} v${LOCAL_VERSION} by Dale Xiao`;
24
26
 
25
27
  let CLI;
26
28
  try {
27
29
  CLI = parseCliArgs(process.argv.slice(2));
28
30
  } catch (e) {
29
31
  console.error(e.message);
30
- console.error("Run ccdx --help for usage.");
32
+ console.error(`Run ${CLI_NAME} --help for usage.`);
31
33
  process.exit(2);
32
34
  }
33
35
 
34
36
  if (CLI.command === "help") {
35
- console.log(cliHelp());
37
+ console.log(cliHelp(CLI_NAME));
36
38
  process.exit(0);
37
39
  }
38
40
  if (CLI.command === "version") {
@@ -43,6 +45,27 @@ if (CLI.command === "usage") {
43
45
  await printUsageSummary();
44
46
  process.exit(0);
45
47
  }
48
+ if (CLI.command === "status") {
49
+ let probe;
50
+ try {
51
+ probe = parseAdapterProbeOptions(process.env);
52
+ } catch (e) {
53
+ console.error(e.message);
54
+ process.exit(2);
55
+ }
56
+ try {
57
+ const snapshot = await readAdapterStatus({
58
+ host: probe.adapterHost,
59
+ port: probe.adapterPort,
60
+ timeoutMs: probe.existingAdapterTimeoutMs,
61
+ });
62
+ console.log(formatAdapterStatus(snapshot, { commandName: CLI_NAME, cliVersion: LOCAL_VERSION }));
63
+ process.exit(0);
64
+ } catch (e) {
65
+ console.error(status("err", e.message));
66
+ process.exit(1);
67
+ }
68
+ }
46
69
 
47
70
  let RUNTIME;
48
71
  try {
@@ -107,7 +130,7 @@ async function refreshClaudeDesktopModelDefs() {
107
130
  const models = JSON.parse(body);
108
131
  const autoReview = codexAutoReviewModelStatus(models);
109
132
  if (!autoReview.available) {
110
- console.log(status("warn", `Auto-review target ${autoReview.upstreamModel} is unavailable: ${autoReview.reason}. Run ccdx doctor --compat to verify the live path.`));
133
+ console.log(status("warn", `Auto-review target ${autoReview.upstreamModel} is unavailable: ${autoReview.reason}. Run ${CLI_NAME} doctor --compat to verify the live path.`));
111
134
  }
112
135
  const gptModelIds = gptModelIdsFromCopilotModels(models);
113
136
  if (gptModelIds.length) {
@@ -169,7 +192,7 @@ async function reuseRunningAdapterIfAvailable() {
169
192
  });
170
193
  if (running.incompatible) {
171
194
  const found = running.data?.version || "unknown";
172
- throw new Error(`Adapter ${found} is already running at ${running.baseUrl}, but this CLI is ${LOCAL_VERSION}. Stop the existing process and run ccdx again.`);
195
+ throw new Error(`Adapter ${found} is already running at ${running.baseUrl}, but this CLI is ${LOCAL_VERSION}. Stop the existing process and run ${CLI_NAME} again.`);
173
196
  }
174
197
  if (!running.ok) return false;
175
198
 
@@ -206,6 +229,7 @@ async function reuseRunningAdapterIfAvailable() {
206
229
 
207
230
  if (CLI.command === "doctor") {
208
231
  const checks = await runDoctor({
232
+ commandName: CLI_NAME,
209
233
  port: ADAPTER_PORT,
210
234
  host: ADAPTER_HOST,
211
235
  online: CLI.online,
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "./cli.mjs";
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.4.44",
3
+ "version": "0.5.0",
4
4
  "description": "Use Codex Desktop, Claude Code, and Claude App with GitHub Copilot",
5
5
  "author": "Dale Xiao",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "ccdx": "bin/cli.mjs",
9
- "codex-copilot-dx": "bin/cli.mjs"
9
+ "codex-copilot-dx": "bin/codex-copilot-dx.mjs"
10
10
  },
11
11
  "files": [
12
12
  "bin/",
@@ -1,6 +1,7 @@
1
1
  const HELP_COMMANDS = new Set(["help", "--help", "-h"]);
2
2
  const VERSION_COMMANDS = new Set(["version", "--version", "-v"]);
3
3
  const START_OPTIONS = new Set(["--configure-claude-desktop", "--show-request-id"]);
4
+ const CLI_COMMANDS = new Set(["ccdx", "codex-copilot-dx"]);
4
5
 
5
6
  function unexpectedArgs(args) {
6
7
  throw new Error(`Unexpected argument${args.length === 1 ? "" : "s"}: ${args.join(" ")}`);
@@ -17,6 +18,10 @@ export function parseCliArgs(args = []) {
17
18
  if (rest.length) unexpectedArgs(rest);
18
19
  return { command: "version", configureClaudeDesktop: false, showRequestId: false, online: false, compat: false };
19
20
  }
21
+ if (command === "status" || command === "--status") {
22
+ if (rest.length) unexpectedArgs(rest);
23
+ return { command: "status", configureClaudeDesktop: false, showRequestId: false, online: false, compat: false };
24
+ }
20
25
  if (command === "usage") {
21
26
  if (rest.length) unexpectedArgs(rest);
22
27
  return { command: "usage", configureClaudeDesktop: false, showRequestId: false, online: false, compat: false };
@@ -80,13 +85,30 @@ export function parseRuntimeOptions(env = process.env) {
80
85
  };
81
86
  }
82
87
 
83
- export function cliHelp() {
88
+ export function parseAdapterProbeOptions(env = process.env) {
89
+ return {
90
+ adapterPort: integerEnv(env, "ADAPTER_PORT", 2026, { min: 1, max: 65535 }),
91
+ adapterHost: String(env.ADAPTER_HOST || "127.0.0.1").trim() || "127.0.0.1",
92
+ existingAdapterTimeoutMs: integerEnv(env, "CCDX_EXISTING_ADAPTER_TIMEOUT_MS", 500, { min: 1 }),
93
+ };
94
+ }
95
+
96
+ export function cliCommandName(executable = process.argv[1]) {
97
+ const name = String(executable || "").split(/[\\/]/).pop();
98
+ if (name === "codex-copilot-dx.mjs") return "codex-copilot-dx";
99
+ return CLI_COMMANDS.has(name) ? name : "ccdx";
100
+ }
101
+
102
+ export function cliHelp(commandName = "ccdx") {
103
+ const name = CLI_COMMANDS.has(commandName) ? commandName : "ccdx";
104
+ const alias = name === "ccdx" ? "codex-copilot-dx" : "ccdx";
84
105
  return `Usage:
85
- ccdx [--configure-claude-desktop] [--show-request-id]
86
- ccdx doctor [--online] [--compat]
87
- ccdx usage
88
- ccdx --version
89
- ccdx --help
106
+ ${name} [--configure-claude-desktop] [--show-request-id]
107
+ ${name} doctor [--online] [--compat]
108
+ ${name} status
109
+ ${name} usage
110
+ ${name} --version
111
+ ${name} --help
90
112
 
91
- Compatibility alias: codex-copilot-dx`;
113
+ Equivalent command: ${alias}`;
92
114
  }
@@ -0,0 +1,122 @@
1
+ import { ADAPTER_STATUS_PATH, adapterBaseUrl } from "./running-adapter.mjs";
2
+ import { status } from "./status.mjs";
3
+
4
+ function finiteNumber(value) {
5
+ const number = Number(value);
6
+ return Number.isFinite(number) ? number : null;
7
+ }
8
+
9
+ function count(value) {
10
+ const number = finiteNumber(value);
11
+ return number === null ? "n/a" : String(Math.max(0, Math.round(number)));
12
+ }
13
+
14
+ function duration(value) {
15
+ const milliseconds = finiteNumber(value);
16
+ if (milliseconds === null) return "n/a";
17
+ const seconds = Math.max(0, Math.round(milliseconds / 1000));
18
+ const days = Math.floor(seconds / 86400);
19
+ const hours = Math.floor((seconds % 86400) / 3600);
20
+ const minutes = Math.floor((seconds % 3600) / 60);
21
+ const remainder = seconds % 60;
22
+ if (days) return `${days}d ${hours}h`;
23
+ if (hours) return `${hours}h ${minutes}m`;
24
+ if (minutes) return `${minutes}m ${remainder}s`;
25
+ return `${remainder}s`;
26
+ }
27
+
28
+ function latency(value) {
29
+ const milliseconds = finiteNumber(value);
30
+ if (milliseconds === null) return "n/a";
31
+ if (milliseconds >= 1000) return `${(milliseconds / 1000).toFixed(2)}s`;
32
+ return `${milliseconds.toFixed(1)}ms`;
33
+ }
34
+
35
+ function mebibytes(value) {
36
+ const bytes = finiteNumber(value);
37
+ return bytes === null ? "n/a" : `${(bytes / (1024 * 1024)).toFixed(1)}MiB`;
38
+ }
39
+
40
+ function tpot(metric) {
41
+ const average = finiteNumber(metric?.avg);
42
+ if (average === null) return "n/a";
43
+ const milliseconds = metric?.unit === "us" ? average / 1000 : average;
44
+ return `${milliseconds.toFixed(2)}ms/token`;
45
+ }
46
+
47
+ function cacheHitRate(cache = {}) {
48
+ const hits = finiteNumber(cache.cache_hits);
49
+ const misses = finiteNumber(cache.cache_misses);
50
+ if (hits === null || misses === null || hits + misses <= 0) return "n/a";
51
+ return `${((hits / (hits + misses)) * 100).toFixed(1)}%`;
52
+ }
53
+
54
+ export async function readAdapterStatus({
55
+ host = "127.0.0.1",
56
+ port = 2026,
57
+ timeoutMs = 500,
58
+ fetchImpl = fetch,
59
+ } = {}) {
60
+ const baseUrl = adapterBaseUrl(host, port);
61
+ const ctrl = new AbortController();
62
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
63
+ try {
64
+ const response = await fetchImpl(`${baseUrl}${ADAPTER_STATUS_PATH}`, {
65
+ headers: { Accept: "application/json" },
66
+ redirect: "error",
67
+ signal: ctrl.signal,
68
+ });
69
+ if (!response.ok) throw new Error(`returned HTTP ${response.status}`);
70
+
71
+ let data;
72
+ try {
73
+ data = await response.json();
74
+ } catch {
75
+ throw new Error("returned invalid JSON");
76
+ }
77
+ if (data?.ok !== true
78
+ || data?.name !== "codex-copilot-dx"
79
+ || typeof data.version !== "string"
80
+ || !Number.isInteger(data.pid)) {
81
+ throw new Error("returned an incompatible status payload");
82
+ }
83
+ return { baseUrl, data };
84
+ } catch (error) {
85
+ const reason = ctrl.signal.aborted
86
+ ? `timed out after ${timeoutMs}ms`
87
+ : error?.cause?.code === "ECONNREFUSED"
88
+ ? "adapter is not running"
89
+ : error?.message || String(error);
90
+ throw new Error(`Could not read adapter status at ${baseUrl}: ${reason}`);
91
+ } finally {
92
+ clearTimeout(timer);
93
+ }
94
+ }
95
+
96
+ export function formatAdapterStatus({ baseUrl, data }, { commandName = "ccdx", cliVersion = data.version } = {}) {
97
+ const requests = data.requests || {};
98
+ const performance = data.stream_performance?.by_route?.responses || {};
99
+ const admission = data.admission || {};
100
+ const history = data.response_history || {};
101
+ const images = data.image_optimization || {};
102
+ const models = data.models || {};
103
+ const copilot = data.copilot || {};
104
+ const limits = data.limits || {};
105
+ const processStats = data.process || {};
106
+ const adapterLine = data.version === cliVersion
107
+ ? status("ok", `Adapter ${data.version} is running at ${baseUrl} (PID ${data.pid}, uptime ${duration(data.uptime_ms)})`)
108
+ : status("warn", `Adapter ${data.version} is running at ${baseUrl}, but this CLI is ${cliVersion}; stop the running adapter before switching versions`);
109
+
110
+ return [
111
+ `${commandName} status`,
112
+ adapterLine,
113
+ status("info", `Requests: ${count(requests.completed)}/${count(requests.total)} completed, ${count(requests.active)} active, ${count(requests.status_4xx)} 4xx, ${count(requests.status_5xx)} 5xx, ${count(requests.aborted)} aborted`),
114
+ status("info", `Responses stream: TTFT avg ${latency(performance.ttft_ms?.avg)} (${count(performance.ttft_ms?.samples)} samples), TPOT avg ${tpot(performance.tpot_us)}`),
115
+ status("info", `Admission: ${count(admission.activeRequests)} active, ${count(admission.queued)} queued, ${count(admission.rejected)} rejected, ${count(admission.timedOut)} timed out, wait avg ${latency(admission.waitMsAvg)}`),
116
+ status("info", `Memory: RSS ${mebibytes(processStats.rss_bytes)}, heap ${mebibytes(processStats.heap_used_bytes)}`),
117
+ status("info", `History: ${mebibytes(history.bytes)} / ${mebibytes(limits.response_history_max_bytes)}, ${count(history.entries)} entries, ${count(history.evicted)} evicted`),
118
+ status("info", `Image cache: ${count(images.cache_entries)} entries, ${cacheHitRate(images)} hits, ${mebibytes(images.cache_bytes)} / ${mebibytes(images.cache_max_bytes)}`),
119
+ status("info", `Models: ${count(models.models)} total, ${count(models.claude_models)} Claude; Copilot token ${copilot.token_cached ? `cached (${duration(copilot.token_expires_in_ms)} remaining)` : "not cached"}`),
120
+ status("info", `Limits: request body ${mebibytes(limits.max_body_bytes)}, decoded body ${mebibytes(limits.max_decoded_body_bytes)}`),
121
+ ].join("\n");
122
+ }
package/src/doctor.mjs CHANGED
@@ -455,7 +455,7 @@ export async function collectDoctorChecks({
455
455
  export async function runDoctor(options = {}) {
456
456
  const log = options.log || console.log;
457
457
  const flags = [options.online ? "--online" : "", options.compat ? "--compat" : ""].filter(Boolean);
458
- log(`ccdx doctor${flags.length ? ` ${flags.join(" ")}` : ""}`);
458
+ log(`${options.commandName || "ccdx"} doctor${flags.length ? ` ${flags.join(" ")}` : ""}`);
459
459
  const checks = await collectDoctorChecks(options);
460
460
  for (const check of checks) log(status(check.kind, check.message));
461
461
  return checks;
@@ -1,11 +1,11 @@
1
1
  import { isIP } from "node:net";
2
- import { adapterHealthPayload } from "./running-adapter.mjs";
2
+ import { ADAPTER_STATUS_PATH, adapterHealthPayload } from "./running-adapter.mjs";
3
3
  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
7
 
8
- export const ADAPTER_STATUS_PATH = "/_ccdx/status";
8
+ export { ADAPTER_STATUS_PATH };
9
9
  const OBSERVABILITY_RUNTIME_CONFIG = loadRuntimeConfig();
10
10
 
11
11
  const ROUTE_NAMES = Object.freeze([
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { localPackageVersion } from "./version.mjs";
3
3
 
4
4
  export const ADAPTER_HEALTH_PATH = "/_ccdx/health";
5
+ export const ADAPTER_STATUS_PATH = "/_ccdx/status";
5
6
  export const ADAPTER_PROTOCOL_VERSION = 2;
6
7
  export const ADAPTER_VERSION = localPackageVersion();
7
8
  const ADAPTER_INSTANCE_ID = randomUUID();