codex-copilot-dx 0.6.6 → 0.6.7
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 +10 -1
- package/bin/cli.mjs +16 -3
- package/package.json +1 -1
- package/src/cli-auth.mjs +99 -3
- package/src/cli-models.mjs +60 -5
- package/src/cli-options.mjs +43 -16
- package/src/cli-table.mjs +114 -0
- package/src/pm-studio-status.mjs +81 -23
- package/src/usage.mjs +104 -9
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
|
|
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.
|
|
@@ -384,6 +389,10 @@ The adapter records token usage metadata to `~/.local/share/codex-copilot-dx/usa
|
|
|
384
389
|
ccdx usage
|
|
385
390
|
```
|
|
386
391
|
|
|
392
|
+
The summary covers the retained current usage log and its single rotated
|
|
393
|
+
backup, not an unbounded lifetime history. Its table reports recorded usage
|
|
394
|
+
responses and per-model input, cache-read, output, and total token metadata.
|
|
395
|
+
|
|
387
396
|
### Debug logging
|
|
388
397
|
|
|
389
398
|
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, {
|
|
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({
|
|
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
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
|
-
|
|
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)
|
|
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 })) };
|
package/src/cli-models.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/cli-options.mjs
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
148
|
-
|
|
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([
|
|
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([
|
|
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
|
+
}
|
package/src/pm-studio-status.mjs
CHANGED
|
@@ -18,6 +18,12 @@ import {
|
|
|
18
18
|
import { readAdapterStatus } from "./cli-status.mjs";
|
|
19
19
|
import { checkRunningAdapter } from "./running-adapter.mjs";
|
|
20
20
|
import { status } from "./status.mjs";
|
|
21
|
+
import {
|
|
22
|
+
cliOutputFormat,
|
|
23
|
+
cliOutputWidth,
|
|
24
|
+
formatResponsiveCliTable,
|
|
25
|
+
terminalCell,
|
|
26
|
+
} from "./cli-table.mjs";
|
|
21
27
|
|
|
22
28
|
const PM_RELAY_HOST = "127.0.0.1";
|
|
23
29
|
const PM_RELAY_PORT = 2026;
|
|
@@ -166,54 +172,106 @@ export async function inspectPmStudioStatus({
|
|
|
166
172
|
return { ok: operational, appPath, app, claude, adapter, runtime };
|
|
167
173
|
}
|
|
168
174
|
|
|
169
|
-
function
|
|
175
|
+
function appStatusItem(result, commandName) {
|
|
170
176
|
const { app } = result;
|
|
171
177
|
const version = app.metadata ? `${app.metadata.version} build ${app.metadata.build}` : "";
|
|
172
178
|
if (app.state === "patched" && app.patchRecord?.valid === true) {
|
|
173
|
-
return
|
|
179
|
+
return { kind: "ok", component: "App patch", detail: `${version}; patched and verified`, message: `PM Studio ${version} is patched and verified against its installed patch record` };
|
|
174
180
|
}
|
|
175
181
|
if (app.state === "patched") {
|
|
176
182
|
const reason = app.patchRecord?.reason || "installed patch record is missing";
|
|
177
|
-
return
|
|
183
|
+
return { kind: "err", component: "App patch", detail: `${version}; patch record not verified`, message: `PM Studio ${version} matches the patch recipe, but its installed patch record is not verified: ${reason}` };
|
|
178
184
|
}
|
|
179
|
-
if (app.state === "clean") return
|
|
180
|
-
if (app.state === "unsupported") return
|
|
185
|
+
if (app.state === "clean") return { kind: "warn", component: "App patch", detail: `${version}; supported, not patched`, message: `PM Studio ${version} is supported but not patched; run ${commandName} pms setup` };
|
|
186
|
+
if (app.state === "unsupported") return { kind: "err", component: "App patch", detail: `${version}; unsupported version`, message: `PM Studio ${version} has no exact patch recipe; no files will be changed` };
|
|
181
187
|
if (app.state === "drift" && app.patchRecipeMatched) {
|
|
182
|
-
return
|
|
188
|
+
return { kind: "err", component: "App patch", detail: `${version}; patch record drift`, message: `PM Studio ${version} matches the patch recipe, but its installed patch record is not verified: ${app.issues.join("; ")}` };
|
|
183
189
|
}
|
|
184
|
-
if (app.state === "drift") return
|
|
185
|
-
if (app.state === "not_installed") return
|
|
186
|
-
return
|
|
190
|
+
if (app.state === "drift") return { kind: "err", component: "App patch", detail: `${version}; integrity drift`, message: `PM Studio ${version} does not match the clean or patched recipe: ${app.issues.join("; ")}` };
|
|
191
|
+
if (app.state === "not_installed") return { kind: "warn", component: "App patch", detail: "not installed", message: `PM Studio is not installed at ${result.appPath}` };
|
|
192
|
+
return { kind: "err", component: "App patch", detail: "inspection failed", message: `PM Studio could not be inspected: ${app.issues.join("; ")}` };
|
|
187
193
|
}
|
|
188
194
|
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
`${commandName} pms status`,
|
|
192
|
-
appStatusLine(result, commandName),
|
|
193
|
-
];
|
|
195
|
+
function pmStudioStatusItems(result, commandName) {
|
|
196
|
+
const items = [appStatusItem(result, commandName)];
|
|
194
197
|
if (result.claude.valid) {
|
|
195
198
|
const account = result.claude.login ? ` for ${result.claude.login}` : "";
|
|
196
|
-
|
|
199
|
+
const detail = result.claude.login ? `${result.claude.login}; isolated profile valid` : "isolated profile valid";
|
|
200
|
+
items.push({ kind: "ok", component: "Claude profile", detail, message: `Isolated Claude profile is valid${account}` });
|
|
197
201
|
} else {
|
|
198
202
|
const reason = result.claude.configured && result.claude.reason ? ` (${result.claude.reason})` : "";
|
|
199
|
-
|
|
203
|
+
items.push({ kind: "err", component: "Claude profile", detail: "not ready", message: `Isolated Claude profile is not ready${reason}; run ${PM_STUDIO_CLAUDE_AUTH_COMMAND}` });
|
|
200
204
|
}
|
|
201
205
|
if (result.adapter?.ok && result.runtime?.ok) {
|
|
202
|
-
|
|
206
|
+
items.push({ kind: "ok", component: "PM relay", detail: `verified at ${result.adapter.baseUrl}`, message: `PM relay and isolated Claude routing are verified at ${result.adapter.baseUrl}` });
|
|
203
207
|
} else if (result.adapter?.ok) {
|
|
204
208
|
const reason = result.runtime?.issues?.join("; ") || "runtime status is unavailable";
|
|
205
|
-
|
|
209
|
+
items.push({ kind: "err", component: "PM relay", detail: "running but routing not ready", message: `The adapter is running at ${result.adapter.baseUrl}, but PM relay routing is not ready: ${reason}; stop it and run ${commandName} start` });
|
|
206
210
|
} else if (result.adapter?.incompatible) {
|
|
207
|
-
|
|
211
|
+
items.push({ kind: "err", component: "PM relay", detail: `incompatible at ${result.adapter.baseUrl}`, message: `The adapter at ${result.adapter.baseUrl} is incompatible; stop it and run ${commandName} start` });
|
|
208
212
|
} else {
|
|
209
|
-
|
|
213
|
+
items.push({ kind: "warn", component: "PM relay", detail: `not running at ${PM_RELAY_HOST}:${PM_RELAY_PORT}`, message: `PM relay is not running at http://${PM_RELAY_HOST}:${PM_RELAY_PORT}; run ${commandName} start` });
|
|
210
214
|
}
|
|
211
215
|
if (result.runtime?.ok) {
|
|
212
|
-
|
|
216
|
+
items.push({ kind: "info", component: "Routing", detail: "GPT -> PM bearer; Claude -> isolated profile", message: "Routing: PM GPT uses the PM Studio bearer; eligible Claude chat uses the isolated Claude profile" });
|
|
213
217
|
} else {
|
|
214
|
-
|
|
218
|
+
items.push({ kind: "info", component: "Routing", detail: "expected: GPT -> PM bearer; Claude -> isolated profile", message: "Expected routing: PM GPT uses the PM Studio bearer; eligible Claude chat uses the isolated Claude profile" });
|
|
219
|
+
}
|
|
220
|
+
return items;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function formatPmStudioStatusPlain(result, commandName, { sanitize = false } = {}) {
|
|
224
|
+
const lines = [
|
|
225
|
+
`${commandName} pms status`,
|
|
226
|
+
...pmStudioStatusItems(result, commandName).map((item) => status(item.kind, item.message)),
|
|
227
|
+
];
|
|
228
|
+
return (sanitize ? lines.map((line) => terminalCell(line, { fallback: "" })) : lines).join("\n");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function tableState(kind) {
|
|
232
|
+
return `[${String(kind).toUpperCase()}]`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function formatPmStudioStatusTable(result, { commandName, output }) {
|
|
236
|
+
const items = pmStudioStatusItems(result, commandName);
|
|
237
|
+
const rows = items.map((item) => ({
|
|
238
|
+
component: item.component,
|
|
239
|
+
state: tableState(item.kind),
|
|
240
|
+
detail: item.detail,
|
|
241
|
+
}));
|
|
242
|
+
const table = formatResponsiveCliTable({
|
|
243
|
+
columns: [
|
|
244
|
+
{ key: "component", label: "COMPONENT" },
|
|
245
|
+
{ key: "state", label: "STATE" },
|
|
246
|
+
{ key: "detail", label: "DETAIL" },
|
|
247
|
+
],
|
|
248
|
+
compactColumns: [
|
|
249
|
+
{ key: "component", label: "COMPONENT" },
|
|
250
|
+
{ key: "state", label: "STATE" },
|
|
251
|
+
],
|
|
252
|
+
rows,
|
|
253
|
+
width: cliOutputWidth(output),
|
|
254
|
+
});
|
|
255
|
+
const lines = [`${commandName} pms status`, table.output];
|
|
256
|
+
const details = table.compact ? items : items.filter((item) => item.kind === "warn" || item.kind === "err");
|
|
257
|
+
if (details.length) {
|
|
258
|
+
lines.push("", "Details:", ...details.map((item) => terminalCell(status(item.kind, item.message), { fallback: "" })));
|
|
259
|
+
}
|
|
260
|
+
return { output: lines.join("\n"), overflow: table.overflow };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function formatPmStudioStatus(result, {
|
|
264
|
+
commandName = "ccdx",
|
|
265
|
+
format = "plain",
|
|
266
|
+
output = process.stdout,
|
|
267
|
+
} = {}) {
|
|
268
|
+
if (cliOutputFormat(format, output) === "plain") {
|
|
269
|
+
return formatPmStudioStatusPlain(result, commandName);
|
|
215
270
|
}
|
|
216
|
-
|
|
271
|
+
const table = formatPmStudioStatusTable(result, { commandName, output });
|
|
272
|
+
return format === "auto" && table.overflow
|
|
273
|
+
? formatPmStudioStatusPlain(result, commandName, { sanitize: true })
|
|
274
|
+
: table.output;
|
|
217
275
|
}
|
|
218
276
|
|
|
219
277
|
export async function runPmStudioStatus(options = {}) {
|
package/src/usage.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import readline from "node:readline";
|
|
5
|
+
import { cliOutputFormat, cliOutputWidth, formatResponsiveCliTable, terminalCell } from "./cli-table.mjs";
|
|
5
6
|
import { parseByteLimit, rotateFileIfNeededSync, rotatedFilePath } from "./file-rotation.mjs";
|
|
6
7
|
|
|
7
8
|
const DEFAULT_USAGE_PATH = path.join(os.homedir(), ".local", "share", "codex-copilot-dx", "usage.jsonl");
|
|
@@ -219,17 +220,111 @@ function fmt(n) {
|
|
|
219
220
|
return Number.isFinite(n) ? n.toLocaleString("en-US") : "0";
|
|
220
221
|
}
|
|
221
222
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
223
|
+
function tableNumber(n) {
|
|
224
|
+
return Number.isFinite(n) ? n.toLocaleString("en-US") : undefined;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function cacheReadTokens(usage = {}) {
|
|
228
|
+
const values = [usage.cache_read_input_tokens, usage.cached_input_tokens]
|
|
229
|
+
.filter((value) => Number.isFinite(value));
|
|
230
|
+
const value = values.length > 0 ? values.reduce((total, current) => total + current, 0) : undefined;
|
|
231
|
+
return Number.isFinite(value) ? value : undefined;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function formatPlainUsageSummary(summary, filePath, { sanitize = false } = {}) {
|
|
235
|
+
const lines = [`Usage log: ${filePath}`];
|
|
225
236
|
if (summary.requests === 0) {
|
|
226
|
-
|
|
227
|
-
return;
|
|
237
|
+
lines.push("No usage records yet.");
|
|
238
|
+
return (sanitize ? lines.map((line) => terminalCell(line, { fallback: "" })) : lines).join("\n");
|
|
228
239
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
240
|
+
lines.push(
|
|
241
|
+
`Requests: ${fmt(summary.requests)}`,
|
|
242
|
+
`Tokens: input=${fmt(summary.totals.input_tokens)} cache_read=${fmt(cacheReadTokens(summary.totals))} output=${fmt(summary.totals.output_tokens)} total=${fmt(summary.totals.total_tokens)}`,
|
|
243
|
+
"",
|
|
244
|
+
"By model:",
|
|
245
|
+
);
|
|
232
246
|
for (const [model, row] of Object.entries(summary.byModel)) {
|
|
233
|
-
|
|
247
|
+
lines.push(` ${model}: requests=${fmt(row.requests)} input=${fmt(row.input_tokens)} cache_read=${fmt(cacheReadTokens(row))} output=${fmt(row.output_tokens)} total=${fmt(row.total_tokens)}`);
|
|
248
|
+
}
|
|
249
|
+
return (sanitize ? lines.map((line) => terminalCell(line, { fallback: "" })) : lines).join("\n");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function compareModelUsage(left, right) {
|
|
253
|
+
const leftTotal = Number.isFinite(left.row.total_tokens) ? left.row.total_tokens : -Infinity;
|
|
254
|
+
const rightTotal = Number.isFinite(right.row.total_tokens) ? right.row.total_tokens : -Infinity;
|
|
255
|
+
if (leftTotal !== rightTotal) return rightTotal - leftTotal;
|
|
256
|
+
return left.model < right.model ? -1 : left.model > right.model ? 1 : 0;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function usageTableRow(model, usage) {
|
|
260
|
+
return {
|
|
261
|
+
model,
|
|
262
|
+
records: tableNumber(usage.requests),
|
|
263
|
+
input: tableNumber(usage.input_tokens),
|
|
264
|
+
cacheRead: tableNumber(cacheReadTokens(usage)),
|
|
265
|
+
output: tableNumber(usage.output_tokens),
|
|
266
|
+
total: tableNumber(usage.total_tokens),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function usageDetailLine(model, usage) {
|
|
271
|
+
return `${terminalCell(model)}: input=${fmt(usage.input_tokens)} cache_read=${fmt(cacheReadTokens(usage))} output=${fmt(usage.output_tokens)}`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function formatTableUsageSummary(summary, filePath, output) {
|
|
275
|
+
const safePath = terminalCell(filePath, { fallback: "" });
|
|
276
|
+
if (summary.requests === 0) return { output: `Usage log: ${safePath}\nNo usage records yet.`, overflow: false };
|
|
277
|
+
const columns = [
|
|
278
|
+
{ key: "model", label: "MODEL" },
|
|
279
|
+
{ key: "records", label: "RECORDS", align: "right" },
|
|
280
|
+
{ key: "input", label: "INPUT", align: "right" },
|
|
281
|
+
{ key: "cacheRead", label: "CACHE READ", align: "right" },
|
|
282
|
+
{ key: "output", label: "OUTPUT", align: "right" },
|
|
283
|
+
{ key: "total", label: "TOTAL", align: "right" },
|
|
284
|
+
];
|
|
285
|
+
const compactColumns = [columns[0], columns[1], columns[5]];
|
|
286
|
+
const modelUsage = Object.entries(summary.byModel)
|
|
287
|
+
.map(([model, row]) => ({ model, row }))
|
|
288
|
+
.sort(compareModelUsage);
|
|
289
|
+
const totalUsage = { requests: summary.requests, ...summary.totals };
|
|
290
|
+
const rows = [usageTableRow("TOTAL", totalUsage), ...modelUsage.map(({ model, row }) => usageTableRow(model, row))];
|
|
291
|
+
const table = formatResponsiveCliTable({
|
|
292
|
+
columns,
|
|
293
|
+
compactColumns,
|
|
294
|
+
rows,
|
|
295
|
+
width: cliOutputWidth(output),
|
|
296
|
+
gap: 1,
|
|
297
|
+
});
|
|
298
|
+
const lines = [`Usage log: ${safePath}`, "", table.output];
|
|
299
|
+
if (table.compact) {
|
|
300
|
+
lines.push(
|
|
301
|
+
"",
|
|
302
|
+
"Details:",
|
|
303
|
+
usageDetailLine("TOTAL", totalUsage),
|
|
304
|
+
...modelUsage.map(({ model, row }) => usageDetailLine(model, row)),
|
|
305
|
+
);
|
|
234
306
|
}
|
|
307
|
+
return { output: lines.join("\n"), overflow: table.overflow };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function formatUsageSummary(summary, {
|
|
311
|
+
filePath = usageLogPath(),
|
|
312
|
+
format = "auto",
|
|
313
|
+
output = process.stdout,
|
|
314
|
+
} = {}) {
|
|
315
|
+
if (cliOutputFormat(format, output) === "plain") return formatPlainUsageSummary(summary, filePath);
|
|
316
|
+
const table = formatTableUsageSummary(summary, filePath, output);
|
|
317
|
+
return format === "auto" && table.overflow
|
|
318
|
+
? formatPlainUsageSummary(summary, filePath, { sanitize: true })
|
|
319
|
+
: table.output;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export async function printUsageSummary({
|
|
323
|
+
filePath = usageLogPath(),
|
|
324
|
+
format = "auto",
|
|
325
|
+
output = process.stdout,
|
|
326
|
+
log = console.log,
|
|
327
|
+
} = {}) {
|
|
328
|
+
const summary = await summarizeUsageLogs(filePath);
|
|
329
|
+
log(formatUsageSummary(summary, { filePath, format, output }));
|
|
235
330
|
}
|