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.
@@ -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 appStatusLine(result, commandName) {
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 status("ok", `PM Studio ${version} is patched and verified against its installed patch record`);
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 status("err", `PM Studio ${version} matches the patch recipe, but its installed patch record is not verified: ${reason}`);
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 status("warn", `PM Studio ${version} is supported but not patched; run ${commandName} pms setup`);
180
- if (app.state === "unsupported") return status("err", `PM Studio ${version} has no exact patch recipe; no files will be changed`);
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 status("err", `PM Studio ${version} matches the patch recipe, but its installed patch record is not verified: ${app.issues.join("; ")}`);
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 status("err", `PM Studio ${version} does not match the clean or patched recipe: ${app.issues.join("; ")}`);
185
- if (app.state === "not_installed") return status("warn", `PM Studio is not installed at ${result.appPath}`);
186
- return status("err", `PM Studio could not be inspected: ${app.issues.join("; ")}`);
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
- export function formatPmStudioStatus(result, { commandName = "ccdx" } = {}) {
190
- const lines = [
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
- lines.push(status("ok", `Isolated Claude profile is valid${account}`));
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
- lines.push(status("err", `Isolated Claude profile is not ready${reason}; run ${PM_STUDIO_CLAUDE_AUTH_COMMAND}`));
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
- lines.push(status("ok", `PM relay and isolated Claude routing are verified at ${result.adapter.baseUrl}`));
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
- lines.push(status("err", `The adapter is running at ${result.adapter.baseUrl}, but PM relay routing is not ready: ${reason}; stop it and run ${commandName} start`));
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
- lines.push(status("err", `The adapter at ${result.adapter.baseUrl} is incompatible; stop it and run ${commandName} start`));
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
- lines.push(status("warn", `PM relay is not running at http://${PM_RELAY_HOST}:${PM_RELAY_PORT}; run ${commandName} start`));
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
- lines.push(status("info", "Routing: PM GPT uses the PM Studio bearer; eligible Claude chat uses the isolated Claude profile"));
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
- lines.push(status("info", "Expected routing: PM GPT uses the PM Studio bearer; eligible Claude chat uses the isolated Claude profile"));
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
- return lines.join("\n");
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
- export async function printUsageSummary() {
223
- const summary = await summarizeUsageLogs();
224
- console.log(`Usage log: ${usageLogPath()}`);
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
- console.log("No usage records yet.");
227
- return;
237
+ lines.push("No usage records yet.");
238
+ return (sanitize ? lines.map((line) => terminalCell(line, { fallback: "" })) : lines).join("\n");
228
239
  }
229
- console.log(`Requests: ${fmt(summary.requests)}`);
230
- console.log(`Tokens: input=${fmt(summary.totals.input_tokens)} cache_read=${fmt(summary.totals.cache_read_input_tokens || summary.totals.cached_input_tokens)} output=${fmt(summary.totals.output_tokens)} total=${fmt(summary.totals.total_tokens)}`);
231
- console.log("\nBy model:");
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
- console.log(` ${model}: requests=${fmt(row.requests)} input=${fmt(row.input_tokens)} cache_read=${fmt(row.cache_read_input_tokens || row.cached_input_tokens)} output=${fmt(row.output_tokens)} total=${fmt(row.total_tokens)}`);
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
  }