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 +11 -1
- package/bin/cli.mjs +30 -6
- package/bin/codex-copilot-dx.mjs +2 -0
- package/package.json +2 -2
- package/src/cli-options.mjs +29 -7
- package/src/cli-status.mjs +122 -0
- package/src/doctor.mjs +1 -1
- package/src/observability.mjs +2 -2
- package/src/running-adapter.mjs +1 -0
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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,
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-copilot-dx",
|
|
3
|
-
"version": "0.
|
|
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/
|
|
9
|
+
"codex-copilot-dx": "bin/codex-copilot-dx.mjs"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"bin/",
|
package/src/cli-options.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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(
|
|
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;
|
package/src/observability.mjs
CHANGED
|
@@ -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
|
|
8
|
+
export { ADAPTER_STATUS_PATH };
|
|
9
9
|
const OBSERVABILITY_RUNTIME_CONFIG = loadRuntimeConfig();
|
|
10
10
|
|
|
11
11
|
const ROUTE_NAMES = Object.freeze([
|
package/src/running-adapter.mjs
CHANGED
|
@@ -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();
|