codex-copilot-dx 0.6.6 → 0.6.8

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
@@ -37,6 +37,10 @@ breaking release. For a one-off run without a global install, use
37
37
  `npm exec --yes --package=codex-copilot-dx@latest -- ccdx`.
38
38
  Run `ccdx <command> --help` (including `auth login claude --help` and
39
39
  `pms status --help`) for command-specific behavior and side-effect details.
40
+ The read-only `usage`, `models`, `auth status`, and `pms status` commands use
41
+ aligned tables in interactive terminals. Redirected or piped output keeps the
42
+ legacy plain-text layout for script compatibility; use `--format table` or
43
+ `--format plain` to choose explicitly.
40
44
 
41
45
  To query the models currently advertised as selectable for the saved Codex
42
46
  Copilot account, run:
@@ -47,7 +51,8 @@ ccdx models
47
51
 
48
52
  This performs a fresh, read-only GitHub Copilot model-directory lookup without
49
53
  starting the adapter, opening device login, reading the local model cache, or
50
- consuming an inference request. It lists model IDs by provider with their
54
+ consuming an inference request. It lists model IDs in a provider, API, and
55
+ preview-status table with their
51
56
  advertised API capabilities and reports the live Claude/Anthropic count. A model
52
57
  being advertised does not guarantee that a later inference request will avoid
53
58
  quota, rate-limit, or policy enforcement.
@@ -111,9 +116,11 @@ active account or profile. GPT requests continue to use the bearer supplied by
111
116
  the current PM profile; exact enabled Anthropic model IDs use the isolated
112
117
  CCDX Claude profile. Neither credential is copied into the other application.
113
118
 
114
- The current patch recipe supports only an exact, unmodified PM Studio 2.9.7 bundle on
115
- macOS. Authorize the isolated Claude profile first, quit PM Studio and its
116
- updater, then run the one-time setup command:
119
+ The current patch recipes support only the exact, unmodified PM Studio 2.9.7
120
+ and 2.9.10 bundles on macOS. This is exact-version support, not a version range:
121
+ 2.9.11 and other unknown builds remain unsupported until they receive their own
122
+ verified recipe. Authorize the isolated Claude profile first, quit PM Studio
123
+ and its updater, then run the one-time setup command:
117
124
 
118
125
  ```bash
119
126
  ccdx auth login claude --reauth --github-login <personal-github-login>
@@ -140,22 +147,31 @@ does not attempt a second automatic write: it preserves the verified backup,
140
147
  retains any diagnostic stage still present, and prints recovery paths. Repeating
141
148
  setup for the exact installed recipe is idempotent.
142
149
 
150
+ The official 2.9.10 artifact can fail macOS's vendor-signature verifier even
151
+ though its signing metadata remains readable. CCDX does not generally ignore
152
+ that failure: the 2.9.10 exception is accepted only when the complete App tree,
153
+ including every file, mode, symlink, and non-volatile extended attribute,
154
+ matches the frozen official fingerprint. The staged and installed CCDX patch
155
+ must still pass strict ad-hoc signature verification and match the complete
156
+ patched-tree record in the backup manifest.
157
+
143
158
  The patch is App-wide: every PM profile sends its Copilot API traffic through
144
159
  the loopback-only `/pm-ccdx/*` relay. Run ordinary `ccdx` before opening PM
145
160
  Studio; if port 2026 is unavailable, PM Copilot requests cannot connect. The
146
- relay explicitly supports only the PM 2.9.7 Copilot paths discovered in the
147
- validated bundle (`GET /models` and `POST /chat/completions`, `/responses`, and
161
+ relay explicitly supports only the Copilot paths discovered in the validated
162
+ 2.9.7 and 2.9.10 bundles (`GET /models` and `POST /chat/completions`, `/responses`, and
148
163
  `/embeddings`) and is not a general-purpose proxy.
149
164
 
150
165
  Patching replaces the vendor signature with an ad-hoc signature. PM Studio's
151
166
  updater is not disabled; an official update may overwrite the patch or may
152
167
  require reinstalling the official App. Setup prints the verified backup and
153
168
  manifest paths plus the version-matched restore procedure. Never restore a
154
- 2.9.7 backup over a newer PM Studio installation.
169
+ backup over a different PM Studio version or build.
155
170
 
156
171
  To restore, first quit PM Studio and its updater. Use the exact verified backup
157
172
  path printed by setup as `BACKUP_APP`, then confirm that both the installed App
158
- and backup still report version/build `2.9.7/2.9.7`:
173
+ and backup still report the same version/build printed by setup (`2.9.7/2.9.7`
174
+ or `2.9.10/2.9.10`):
159
175
 
160
176
  ```bash
161
177
  BACKUP_APP="<verified-backup-path-from-ccdx-pms-setup>"
@@ -165,8 +181,8 @@ BACKUP_APP="<verified-backup-path-from-ccdx-pms-setup>"
165
181
  /usr/bin/plutil -extract CFBundleVersion raw -o - "$BACKUP_APP/Contents/Info.plist"
166
182
  ```
167
183
 
168
- Only when all four values match `2.9.7`, move the patched App aside, restore the
169
- complete vendor-signed backup, and verify it before launch:
184
+ Only when all four values match the one exact recipe, move the patched App
185
+ aside, restore the complete vendor bundle, and verify it before launch:
170
186
 
171
187
  ```bash
172
188
  RECOVERY_DIR="$(/usr/bin/mktemp -d '/Applications/CCDX-PM-Studio-recovery.XXXXXX')"
@@ -174,12 +190,16 @@ PATCHED_APP="$RECOVERY_DIR/PM Studio.app"
174
190
  mv "/Applications/PM Studio.app" "$PATCHED_APP"
175
191
  /usr/bin/ditto --rsrc --extattr --acl "$BACKUP_APP" "/Applications/PM Studio.app"
176
192
  /usr/bin/codesign --verify --deep --strict --verbose=2 "/Applications/PM Studio.app"
193
+ ccdx pms status
177
194
  ```
178
195
 
179
- If the version check, copy, or signature verification fails, do not launch the
180
- restored App. Keep the moved patched bundle at `$PATCHED_APP` and reinstall the official PM Studio
181
- instead of applying a 2.9.7 backup to unknown content. CCDX never runs these
182
- restore commands or `sudo` automatically.
196
+ For 2.9.10, the vendor `codesign` command may report the documented upstream
197
+ signature failure; in that case the App-patch row from `ccdx pms status` must
198
+ still classify the restored bundle as exact supported clean content rather than
199
+ integrity drift. If the version check, copy, or both verification paths fail,
200
+ do not launch the restored App. Keep the moved patched bundle at `$PATCHED_APP`
201
+ and reinstall the official PM Studio instead of applying a version-mismatched
202
+ backup. CCDX never runs these restore commands or `sudo` automatically.
183
203
 
184
204
  To change the model used by Codex Auto-review, run:
185
205
 
@@ -384,6 +404,10 @@ The adapter records token usage metadata to `~/.local/share/codex-copilot-dx/usa
384
404
  ccdx usage
385
405
  ```
386
406
 
407
+ The summary covers the retained current usage log and its single rotated
408
+ backup, not an unbounded lifetime history. Its table reports recorded usage
409
+ responses and per-model input, cache-read, output, and total token metadata.
410
+
387
411
  ### Debug logging
388
412
 
389
413
  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.
package/bin/cli.mjs CHANGED
@@ -47,7 +47,10 @@ if (CLI.command === "version") {
47
47
  process.exit(0);
48
48
  }
49
49
  if (CLI.command === "usage") {
50
- await printUsageSummary();
50
+ await printUsageSummary({
51
+ format: CLI.outputFormat || "auto",
52
+ output: process.stdout,
53
+ });
51
54
  process.exit(0);
52
55
  }
53
56
  if (CLI.command === "auth") {
@@ -59,6 +62,8 @@ if (CLI.command === "auth") {
59
62
  reauth: CLI.reauth,
60
63
  expectedLogin: CLI.expectedLogin,
61
64
  commandName: CLI_NAME,
65
+ format: CLI.outputFormat || "auto",
66
+ output: process.stdout,
62
67
  });
63
68
  if (result.output) console.log(result.output);
64
69
  if (result.action === "login") {
@@ -97,7 +102,11 @@ if (CLI.command === "status") {
97
102
  if (CLI.command === "models") {
98
103
  try {
99
104
  const catalog = await fetchLiveCopilotModels({ profile: CLI.profile });
100
- console.log(formatLiveCopilotModels(catalog, { commandName: CLI_NAME }));
105
+ console.log(formatLiveCopilotModels(catalog, {
106
+ commandName: CLI_NAME,
107
+ format: CLI.outputFormat || "auto",
108
+ output: process.stdout,
109
+ }));
101
110
  process.exit(0);
102
111
  } catch (e) {
103
112
  console.error(status("err", e.message));
@@ -127,7 +136,11 @@ if (CLI.command === "pms") {
127
136
  try {
128
137
  if (CLI.action === "status") {
129
138
  const { runPmStudioStatus } = await import("../src/pm-studio-status.mjs");
130
- const result = await runPmStudioStatus({ commandName: CLI_NAME });
139
+ const result = await runPmStudioStatus({
140
+ commandName: CLI_NAME,
141
+ format: CLI.outputFormat || "auto",
142
+ output: process.stdout,
143
+ });
131
144
  process.exit(result.ok ? 0 : 1);
132
145
  }
133
146
  const { runPmStudioSetup } = await import("../src/pm-studio-setup.mjs");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.6.6",
3
+ "version": "0.6.8",
4
4
  "description": "Use Codex Desktop, Claude Code, and Claude App with GitHub Copilot",
5
5
  "author": "Dale Xiao",
6
6
  "type": "module",
package/src/cli-auth.mjs CHANGED
@@ -24,6 +24,12 @@ import { saveModelCache } from "./model-cache.mjs";
24
24
  import { isClaudeCopilotModel } from "./models.mjs";
25
25
  import { profileRouting } from "./profile-routing.mjs";
26
26
  import { status } from "./status.mjs";
27
+ import {
28
+ cliOutputFormat,
29
+ cliOutputWidth,
30
+ formatResponsiveCliTable,
31
+ terminalCell,
32
+ } from "./cli-table.mjs";
27
33
 
28
34
  const CLIENT_ID = "Iv1.b507a08c87ecfe98";
29
35
  const SCOPE = "read:user";
@@ -479,7 +485,7 @@ function onlineStatusLine(name, profile) {
479
485
  return status("ok", `${name} online: ${account}${online.models} models, ${online.claudeModels} Claude`);
480
486
  }
481
487
 
482
- export function formatAuthStatus(snapshot = authStatus(), { commandName = "ccdx" } = {}) {
488
+ function authStatusPlainLines(snapshot, { commandName }) {
483
489
  const claudeLabel = snapshot.profiles.claude.configured
484
490
  ? `${accountLabel(snapshot.profiles.claude)} [isolated profile]`
485
491
  : "inherits Codex [no isolated profile]";
@@ -493,7 +499,95 @@ export function formatAuthStatus(snapshot = authStatus(), { commandName = "ccdx"
493
499
  status(claudeKind, `Claude: ${claudeLabel}`),
494
500
  onlineStatusLine("Claude", snapshot.profiles.claude),
495
501
  status("info", `Routing: responses -> ${snapshot.routing.responses}; messages -> ${snapshot.routing.messages}`),
496
- ].filter(Boolean).join("\n");
502
+ ].filter(Boolean);
503
+ }
504
+
505
+ function authTableOnline(profile) {
506
+ const online = profile.online;
507
+ if (!online) return { state: "[INFO] not checked", models: "—", claudeModels: "—" };
508
+ if (online.inherited) return { state: "[INFO] inherits Codex", models: "—", claudeModels: "—" };
509
+ if (!online.ok) {
510
+ const httpStatus = online.httpStatus ? ` (HTTP ${online.httpStatus})` : "";
511
+ return {
512
+ state: `[WARN] ${online.reason || "unavailable"}${httpStatus}`,
513
+ models: "—",
514
+ claudeModels: "—",
515
+ };
516
+ }
517
+ return {
518
+ state: `[OK] verified${online.login ? ` as ${online.login}` : ""}`,
519
+ models: Number.isFinite(online.models) ? String(online.models) : "—",
520
+ claudeModels: Number.isFinite(online.claudeModels) ? String(online.claudeModels) : "—",
521
+ };
522
+ }
523
+
524
+ function authTableRow(name, profile, { claude = false } = {}) {
525
+ const inherited = claude && !profile.configured;
526
+ const online = authTableOnline(profile);
527
+ const local = inherited
528
+ ? "[INFO] inherits Codex"
529
+ : profile.valid
530
+ ? "[OK] ready"
531
+ : `[WARN] ${accountLabel(profile)}`;
532
+ const account = inherited ? "inherits Codex" : accountLabel(profile);
533
+ const mode = inherited ? "inherited" : claude ? "isolated" : "legacy path";
534
+ return {
535
+ profile: name,
536
+ account,
537
+ mode,
538
+ local,
539
+ online: online.state,
540
+ models: online.models,
541
+ claudeModels: online.claudeModels,
542
+ };
543
+ }
544
+
545
+ export function formatAuthStatus(snapshot = authStatus(), {
546
+ commandName = "ccdx",
547
+ format = "plain",
548
+ output = process.stdout,
549
+ width = cliOutputWidth(output),
550
+ } = {}) {
551
+ const plainLines = authStatusPlainLines(snapshot, { commandName });
552
+ if (cliOutputFormat(format, output) === "plain") {
553
+ return plainLines.join("\n");
554
+ }
555
+
556
+ const rows = [
557
+ authTableRow("Codex", snapshot.profiles.codex),
558
+ authTableRow("Claude", snapshot.profiles.claude, { claude: true }),
559
+ ];
560
+ const table = formatResponsiveCliTable({
561
+ columns: [
562
+ { key: "profile", label: "PROFILE" },
563
+ { key: "account", label: "ACCOUNT" },
564
+ { key: "mode", label: "MODE" },
565
+ { key: "local", label: "LOCAL" },
566
+ { key: "online", label: "ONLINE" },
567
+ { key: "models", label: "MODELS", align: "right" },
568
+ { key: "claudeModels", label: "CLAUDE", align: "right" },
569
+ ],
570
+ compactColumns: [
571
+ { key: "profile", label: "PROFILE" },
572
+ { key: "local", label: "LOCAL" },
573
+ { key: "online", label: "ONLINE" },
574
+ ],
575
+ rows,
576
+ width,
577
+ });
578
+ if (format === "auto" && table.overflow) {
579
+ return plainLines.map((line) => terminalCell(line, { fallback: "" })).join("\n");
580
+ }
581
+ const lines = [
582
+ `${commandName} auth status`,
583
+ "",
584
+ table.output,
585
+ ];
586
+ if (table.compact) {
587
+ lines.push("", "Details:", ...plainLines.slice(1, -1).map((line) => terminalCell(line, { fallback: "" })));
588
+ }
589
+ lines.push(terminalCell(plainLines.at(-1), { fallback: "" }));
590
+ return lines.join("\n");
497
591
  }
498
592
 
499
593
  export async function runAuthCommand({
@@ -503,11 +597,13 @@ export async function runAuthCommand({
503
597
  reauth = false,
504
598
  expectedLogin = "",
505
599
  commandName = "ccdx",
600
+ format = "plain",
601
+ output = process.stdout,
506
602
  ...options
507
603
  } = {}) {
508
604
  if (action === "status") {
509
605
  const snapshot = online ? await authStatusOnline(options) : authStatus(options);
510
- return { action, output: formatAuthStatus(snapshot, { commandName }), snapshot };
606
+ return { action, output: formatAuthStatus(snapshot, { commandName, format, output }), snapshot };
511
607
  }
512
608
  if (profile !== AUTH_PROFILE_CLAUDE) throw new Error("Only the isolated Claude profile can be changed by this command");
513
609
  if (action === "login") return { action, ...(await authorizeClaudeProfile({ ...options, reauth, expectedLogin })) };
@@ -11,6 +11,12 @@ import {
11
11
  isClaudeCopilotModel,
12
12
  } from "./models.mjs";
13
13
  import { status } from "./status.mjs";
14
+ import {
15
+ cliOutputFormat,
16
+ cliOutputWidth,
17
+ formatResponsiveCliTable,
18
+ terminalCell,
19
+ } from "./cli-table.mjs";
14
20
 
15
21
  const DEFAULT_MODELS_TIMEOUT_MS = 10000;
16
22
  const SUPPORTED_ENDPOINTS = [
@@ -166,21 +172,22 @@ export async function fetchLiveCopilotModels({
166
172
  }
167
173
  }
168
174
 
169
- export function formatLiveCopilotModels(catalog, { commandName = "ccdx" } = {}) {
170
- const models = Array.isArray(catalog?.models) ? catalog.models : [];
171
- const profile = catalog?.profile === AUTH_PROFILE_CLAUDE ? AUTH_PROFILE_CLAUDE : AUTH_PROFILE_CODEX;
175
+ function liveCatalogHeader(catalog, { commandName, profile, models }) {
172
176
  const responses = models.filter((model) => model.endpoints.includes("responses")).length;
173
177
  const chat = models.filter((model) => model.endpoints.includes("chat")).length;
174
178
  const claude = models.filter(isClaudeCopilotCatalogEntry).length;
175
- const lines = [
179
+ return [
176
180
  `${commandName} models${profile === AUTH_PROFILE_CLAUDE ? " --profile claude" : ""}`,
177
181
  status("ok", `Live catalog from ${safeText(catalog?.upstreamHost, "GitHub Copilot")}: ${models.length} selectable of ${Number(catalog?.advertised) || 0} advertised`),
178
182
  status("info", `Responses: ${responses}; Chat: ${chat}; Claude/Anthropic: ${claude}`),
179
183
  ];
184
+ }
180
185
 
186
+ function formatLiveCopilotModelsPlain(catalog, { commandName, profile, models }, { sanitize = false } = {}) {
187
+ const lines = liveCatalogHeader(catalog, { commandName, profile, models });
181
188
  if (!models.length) {
182
189
  lines.push(status("warn", "No selectable models were advertised for this account"));
183
- return lines.join("\n");
190
+ return (sanitize ? lines.map((line) => terminalCell(line, { fallback: "" })) : lines).join("\n");
184
191
  }
185
192
 
186
193
  let vendor = "";
@@ -192,5 +199,53 @@ export function formatLiveCopilotModels(catalog, { commandName = "ccdx" } = {})
192
199
  const flags = [...model.endpoints, ...(model.preview ? ["preview"] : [])];
193
200
  lines.push(` ${model.id} [${flags.join(", ")}]`);
194
201
  }
202
+ return (sanitize ? lines.map((line) => terminalCell(line, { fallback: "" })) : lines).join("\n");
203
+ }
204
+
205
+ export function formatLiveCopilotModels(catalog, {
206
+ commandName = "ccdx",
207
+ format = "plain",
208
+ output = process.stdout,
209
+ width = cliOutputWidth(output),
210
+ } = {}) {
211
+ const models = Array.isArray(catalog?.models) ? catalog.models : [];
212
+ const profile = catalog?.profile === AUTH_PROFILE_CLAUDE ? AUTH_PROFILE_CLAUDE : AUTH_PROFILE_CODEX;
213
+ const context = { commandName, profile, models };
214
+ if (cliOutputFormat(format, output) === "plain") {
215
+ return formatLiveCopilotModelsPlain(catalog, context);
216
+ }
217
+
218
+ const lines = liveCatalogHeader(catalog, context);
219
+ if (!models.length) {
220
+ lines.push(status("warn", "No selectable models were advertised for this account"));
221
+ return lines.join("\n");
222
+ }
223
+
224
+ const rows = models.map((model) => ({
225
+ vendor: model.vendor,
226
+ model: model.id,
227
+ vendorModel: `${model.vendor}/${model.id}`,
228
+ apis: model.endpoints.join(", "),
229
+ preview: model.preview ? "yes" : "no",
230
+ }));
231
+ const table = formatResponsiveCliTable({
232
+ columns: [
233
+ { key: "vendor", label: "VENDOR" },
234
+ { key: "model", label: "MODEL" },
235
+ { key: "apis", label: "APIS" },
236
+ { key: "preview", label: "PREVIEW" },
237
+ ],
238
+ compactColumns: [
239
+ { key: "vendorModel", label: "VENDOR/MODEL" },
240
+ { key: "apis", label: "APIS" },
241
+ { key: "preview", label: "PREVIEW" },
242
+ ],
243
+ rows,
244
+ width,
245
+ });
246
+ if (format === "auto" && table.overflow) {
247
+ return formatLiveCopilotModelsPlain(catalog, context, { sanitize: true });
248
+ }
249
+ lines.push("", table.output);
195
250
  return lines.join("\n");
196
251
  }
@@ -60,6 +60,18 @@ function checkedProfile(value, { allowAll = false } = {}) {
60
60
  return profile;
61
61
  }
62
62
 
63
+ function checkedOutputFormat(value) {
64
+ const format = value || "";
65
+ if (!new Set(["table", "plain"]).has(format)) {
66
+ throw new Error(`Format must be table or plain: ${value}`);
67
+ }
68
+ return format;
69
+ }
70
+
71
+ function outputFormatOption(value) {
72
+ return value ? { outputFormat: checkedOutputFormat(value) } : {};
73
+ }
74
+
63
75
  function baseCommand(command, extra = {}) {
64
76
  return {
65
77
  command,
@@ -137,28 +149,42 @@ export function parseCliArgs(args = []) {
137
149
  return baseCommand("status");
138
150
  }
139
151
  if (command === "usage") {
140
- if (rest.length) unexpectedArgs(rest);
141
- return baseCommand("usage");
152
+ const options = parseOptions(rest, new Map([["--format", "value"]]));
153
+ return baseCommand("usage", outputFormatOption(options["--format"]));
142
154
  }
143
155
  if (PM_COMMANDS.has(command)) {
144
156
  const [action, ...pmsArgs] = rest;
145
157
  if (!action) throw new Error("Missing pms action: expected setup or status");
146
158
  if (!new Set(["setup", "status"]).has(action)) throw new Error(`Unknown pms action: ${action}`);
147
- if (pmsArgs.length) unexpectedArgs(pmsArgs);
148
- return baseCommand("pms", { action });
159
+ if (action === "setup") {
160
+ if (pmsArgs.length) unexpectedArgs(pmsArgs);
161
+ return baseCommand("pms", { action });
162
+ }
163
+ const options = parseOptions(pmsArgs, new Map([["--format", "value"]]));
164
+ return baseCommand("pms", {
165
+ action,
166
+ ...outputFormatOption(options["--format"]),
167
+ });
149
168
  }
150
169
  if (command === "models") {
151
- const options = parseOptions(rest, new Map([["--profile", "value"]]));
170
+ const options = parseOptions(rest, new Map([
171
+ ["--profile", "value"],
172
+ ["--format", "value"],
173
+ ]));
152
174
  return {
153
175
  ...baseCommand("models"),
154
176
  profile: checkedProfile(options["--profile"]),
177
+ ...outputFormatOption(options["--format"]),
155
178
  };
156
179
  }
157
180
  if (command === "auth") {
158
181
  const [action, ...authArgs] = rest;
159
182
  if (!action) throw new Error("Missing auth action: expected status or login");
160
183
  if (action === "status") {
161
- const options = parseOptions(authArgs, new Map([["--online", "flag"]]));
184
+ const options = parseOptions(authArgs, new Map([
185
+ ["--online", "flag"],
186
+ ["--format", "value"],
187
+ ]));
162
188
  return {
163
189
  ...baseCommand("auth"),
164
190
  action: "status",
@@ -166,6 +192,7 @@ export function parseCliArgs(args = []) {
166
192
  online: Boolean(options["--online"]),
167
193
  expectedLogin: "",
168
194
  reauth: false,
195
+ ...outputFormatOption(options["--format"]),
169
196
  };
170
197
  }
171
198
  if (action === "login") {
@@ -259,16 +286,16 @@ export function cliCommandName() {
259
286
  function topicHelp(name, topic) {
260
287
  const sections = {
261
288
  start: `Usage:\n ${name} [start] [--configure-claude-app] [--show-request-id]\n\nStarts or reuses the local adapter, updates Codex and Claude Code configuration, and attempts to open Codex or ChatGPT when auto-launch is enabled and supported.\n--configure-claude-app also creates or updates the managed Claude App gateway profile.`,
262
- auth: `Usage:\n ${name} auth status [--online]\n ${name} auth login claude [--github-login <login>] [--reauth]\n\nShows the two account profiles or configures the isolated Claude account. Device login starts only when no reusable local Copilot credential is available or --reauth is used.`,
263
- "auth status": `Usage:\n ${name} auth status [--online]\n\nShows account routing without exposing credentials. --online also verifies both configured Copilot entitlements and model catalogs.`,
289
+ auth: `Usage:\n ${name} auth status [--online] [--format table|plain]\n ${name} auth login claude [--github-login <login>] [--reauth]\n\nShows the two account profiles or configures the isolated Claude account. Device login starts only when no reusable local Copilot credential is available or --reauth is used.`,
290
+ "auth status": `Usage:\n ${name} auth status [--online] [--format table|plain]\n\nShows account routing without exposing credentials. --online also verifies both configured Copilot entitlements and model catalogs. Interactive terminals use a table by default.`,
264
291
  "auth login": `Usage:\n ${name} auth login claude [--github-login <login>] [--reauth]\n\nReuses a compatible local Copilot credential when possible. --reauth forces GitHub Device Flow.`,
265
292
  doctor: `Usage:\n ${name} doctor [--online] [--compat] [--profile codex|claude|all]\n\nChecks local credentials, client configuration, PM Studio state, and the adapter. --online validates account entitlement; --compat sends minimal inference requests and consumes a small amount of Copilot usage.`,
266
293
  status: `Usage:\n ${name} status\n\nShows bounded runtime, routing, performance, cache, queue, and PM relay metrics from a running adapter.`,
267
- models: `Usage:\n ${name} models [--profile codex|claude]\n\nPerforms a fresh, read-only Copilot model-directory lookup for one saved profile.`,
268
- pms: `Usage:\n ${name} pms status\n ${name} pms setup\n ${name} pm-studio status\n ${name} pm-studio setup\n\nInspects or installs the version-locked PM Studio loopback patch. setup modifies the installed app only after all preflight checks pass.`,
294
+ models: `Usage:\n ${name} models [--profile codex|claude] [--format table|plain]\n\nPerforms a fresh, read-only Copilot model-directory lookup for one saved profile. Interactive terminals use a table by default.`,
295
+ pms: `Usage:\n ${name} pms status [--format table|plain]\n ${name} pms setup\n ${name} pm-studio status [--format table|plain]\n ${name} pm-studio setup\n\nInspects or installs the version-locked PM Studio loopback patch. setup modifies the installed app only after all preflight checks pass.`,
269
296
  "pms setup": `Usage:\n ${name} pms setup\n\nBacks up, patches, verifies, and ad-hoc signs an exactly supported PM Studio bundle. Unknown versions and drift are never modified.`,
270
- "pms status": `Usage:\n ${name} pms status\n\nRead-only inspection of the installed PM Studio version, patch integrity, Claude profile, and relay availability.`,
271
- usage: `Usage:\n ${name} usage\n\nSummarizes local token usage metadata without reading prompt or completion content.`,
297
+ "pms status": `Usage:\n ${name} pms status [--format table|plain]\n\nRead-only inspection of the installed PM Studio version, patch integrity, Claude profile, and relay availability. Interactive terminals use a table by default.`,
298
+ 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.`,
272
299
  "auto-review-model": `Usage:\n ${name} auto-review-model\n\nInteractively selects an enabled Responses model for Codex Auto-review.`,
273
300
  update: `Usage:\n ${name} update [npm|github]\n\nUpdates the global package from the configured npm registry or GitHub main. With no source, an interactive terminal prompts for one.`,
274
301
  version: `Usage:\n ${name} --version`,
@@ -282,14 +309,14 @@ export function cliHelp(commandName = "ccdx", topic = "") {
282
309
  if (normalizedTopic) return topicHelp(name, normalizedTopic);
283
310
  return `Usage:
284
311
  ${name} [start] [--configure-claude-app] [--show-request-id]
285
- ${name} auth status [--online]
312
+ ${name} auth status [--online] [--format table|plain]
286
313
  ${name} auth login claude [--github-login <login>] [--reauth]
287
314
  ${name} doctor [--online] [--compat] [--profile codex|claude|all]
288
315
  ${name} status
289
- ${name} models [--profile codex|claude]
290
- ${name} pms status
316
+ ${name} models [--profile codex|claude] [--format table|plain]
317
+ ${name} pms status [--format table|plain]
291
318
  ${name} pms setup
292
- ${name} usage
319
+ ${name} usage [--format table|plain]
293
320
  ${name} auto-review-model
294
321
  ${name} update [npm|github]
295
322
  ${name} --version
@@ -0,0 +1,114 @@
1
+ import { stripVTControlCharacters } from "node:util";
2
+
3
+ const DEFAULT_TERMINAL_COLUMNS = 120;
4
+ const COMBINING_MARK = /\p{Mark}/u;
5
+ const EMOJI = /\p{Extended_Pictographic}|\p{Regional_Indicator}/u;
6
+ const SIMPLE_WIDTH = /^[\u0020-\u007e\u2014]*$/;
7
+ let graphemeSegmenter;
8
+
9
+ function wideCodePoint(codePoint) {
10
+ return codePoint >= 0x1100 && (
11
+ codePoint <= 0x115f
12
+ || codePoint === 0x2329
13
+ || codePoint === 0x232a
14
+ || (codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f)
15
+ || (codePoint >= 0xac00 && codePoint <= 0xd7a3)
16
+ || (codePoint >= 0xf900 && codePoint <= 0xfaff)
17
+ || (codePoint >= 0xfe10 && codePoint <= 0xfe19)
18
+ || (codePoint >= 0xfe30 && codePoint <= 0xfe6f)
19
+ || (codePoint >= 0xff00 && codePoint <= 0xff60)
20
+ || (codePoint >= 0xffe0 && codePoint <= 0xffe6)
21
+ || (codePoint >= 0x20000 && codePoint <= 0x3fffd)
22
+ );
23
+ }
24
+
25
+ function graphemeWidth(segment) {
26
+ if (EMOJI.test(segment)) return 2;
27
+ let width = 0;
28
+ for (const character of segment) {
29
+ const codePoint = character.codePointAt(0);
30
+ if (codePoint === 0x200d || (codePoint >= 0xfe00 && codePoint <= 0xfe0f) || COMBINING_MARK.test(character)) continue;
31
+ width += wideCodePoint(codePoint) ? 2 : 1;
32
+ }
33
+ return width;
34
+ }
35
+
36
+ function displayWidth(value) {
37
+ if (SIMPLE_WIDTH.test(value)) return value.length;
38
+ graphemeSegmenter ||= new Intl.Segmenter("en", { granularity: "grapheme" });
39
+ let width = 0;
40
+ for (const { segment } of graphemeSegmenter.segment(value)) width += graphemeWidth(segment);
41
+ return width;
42
+ }
43
+
44
+ function padding(length) {
45
+ return length > 0 ? " ".repeat(length) : "";
46
+ }
47
+
48
+ function padded(value, width, align) {
49
+ const spaces = padding(width - displayWidth(value));
50
+ return align === "right" ? `${spaces}${value}` : `${value}${spaces}`;
51
+ }
52
+
53
+ export function terminalCell(value, { fallback = "—" } = {}) {
54
+ if (value === undefined || value === null) return fallback;
55
+ const text = stripVTControlCharacters(String(value))
56
+ .replace(/[\u0000-\u001f\u007f-\u009f]/g, " ")
57
+ .replace(/[\u061c\u200b\u200e\u200f\u202a-\u202e\u2060\u2066-\u2069\ufeff]/g, "")
58
+ .replace(/\s+/g, " ")
59
+ .trim();
60
+ return text || fallback;
61
+ }
62
+
63
+ export function cliOutputFormat(format = "auto", output = process.stdout) {
64
+ if (format === "table" || format === "plain") return format;
65
+ return output?.isTTY === true ? "table" : "plain";
66
+ }
67
+
68
+ export function cliOutputWidth(output = process.stdout) {
69
+ const columns = Number(output?.columns);
70
+ return Number.isSafeInteger(columns) && columns > 0 ? columns : DEFAULT_TERMINAL_COLUMNS;
71
+ }
72
+
73
+ export function formatCliTable({ columns, rows, gap = 2 }) {
74
+ const normalizedColumns = columns.map((column) => ({
75
+ ...column,
76
+ label: terminalCell(column.label, { fallback: "" }),
77
+ }));
78
+ const normalizedRows = rows.map((row) => Object.fromEntries(normalizedColumns.map((column) => [
79
+ column.key,
80
+ terminalCell(row[column.key]),
81
+ ])));
82
+ const widths = normalizedColumns.map((column) => Math.max(
83
+ displayWidth(column.label),
84
+ ...normalizedRows.map((row) => displayWidth(row[column.key])),
85
+ ));
86
+ const line = (values) => normalizedColumns
87
+ .map((column, index) => padded(values[column.key], widths[index], column.align))
88
+ .join(" ".repeat(gap))
89
+ .trimEnd();
90
+ const header = Object.fromEntries(normalizedColumns.map((column) => [column.key, column.label]));
91
+ const separator = Object.fromEntries(normalizedColumns.map((column, index) => [
92
+ column.key,
93
+ "-".repeat(widths[index]),
94
+ ]));
95
+ return [line(header), line(separator), ...normalizedRows.map(line)].join("\n");
96
+ }
97
+
98
+ export function cliTableWidth(table) {
99
+ return Math.max(0, ...String(table).split("\n").map(displayWidth));
100
+ }
101
+
102
+ export function formatResponsiveCliTable({ columns, compactColumns, rows, width = DEFAULT_TERMINAL_COLUMNS, gap = 2 }) {
103
+ const full = formatCliTable({ columns, rows, gap });
104
+ const fullWidth = cliTableWidth(full);
105
+ if (!compactColumns?.length || fullWidth <= width) {
106
+ return { output: full, compact: false, overflow: fullWidth > width };
107
+ }
108
+ const compact = formatCliTable({ columns: compactColumns, rows, gap });
109
+ return {
110
+ output: compact,
111
+ compact: true,
112
+ overflow: cliTableWidth(compact) > width,
113
+ };
114
+ }