@tokensrc/codex 1.14.22 → 1.14.24

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/dist/commands.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { hostname } from "node:os";
3
3
  import { join } from "node:path";
4
+ import { PACKAGE_VERSION } from "./branding.js";
4
5
  import { getStoragePath, loadAccounts, setStoragePath, setStoragePathDirect } from "codex-multi-auth/storage";
5
6
  import { normalizeCodexServiceUrl } from "./client.js";
6
7
  import { CodexServiceError } from "./errors.js";
@@ -19,7 +20,9 @@ import { clearCodexSelection, loadCodexSelection, saveCodexSelection } from "./s
19
20
  import { installCodexShortcuts, removeCodexShortcuts } from "./shortcuts.js";
20
21
  import { isCodexAccountAvailable, isCodexSubscriptionExpired, renderCodexAccountDashboard, selectCodexAccount } from "./tui.js";
21
22
  import { ensureCodexV6DeviceCredentials, reconcileCodexV6DeviceAccess, reportCodexV6DeviceCredentials, requireAvailableCodexV6DeviceAccess } from "./request-bound-device-v6-flow.js";
22
- import { resetCodexV6LocalState } from "./request-bound-device-v6-state.js";
23
+ import { getOrCreateCodexInstallationId } from "./multi-device-v5-state.js";
24
+ import { readCodexV6BindingState, resetCodexV6LocalState } from "./request-bound-device-v6-state.js";
25
+ import { probeCodexV6OnlineCredentialHealth, reportCodexV6OnlineCredentialHealth } from "./request-bound-device-v6-health.js";
23
26
  export const NO_AVAILABLE_CHATGPT_ACCOUNT_MESSAGE = "当前用户没有绑定可用的 ChatGPT 账号。请前往 OA 系统提交 IT11 工单,申请独享或共享的 ChatGPT 个人账号。";
24
27
  const SCHEMA_V2_LEGACY_MANAGEMENT_DISABLED = "schema_v2_legacy_management_disabled";
25
28
  function findV3Assignment(input) {
@@ -36,6 +39,30 @@ async function json(command, deps) {
36
39
  function print(output, value) {
37
40
  output.write(`${JSON.stringify(value, null, 2)}\n`);
38
41
  }
42
+ function safeDiagnostic(deps, stage, details = {}) {
43
+ if (!deps.safeStartupDiagnostics)
44
+ return;
45
+ const safeDetails = Object.fromEntries(Object.entries(details).filter(([, value]) => value !== undefined));
46
+ deps.errorOutput.write(`[numa-debug] ${JSON.stringify({ stage, ...safeDetails })}\n`);
47
+ }
48
+ async function withSafeDiagnostic(deps, stage, operation) {
49
+ const startedAt = Date.now();
50
+ safeDiagnostic(deps, `${stage}.started`);
51
+ try {
52
+ const result = await operation();
53
+ safeDiagnostic(deps, `${stage}.completed`, { elapsedMs: Date.now() - startedAt });
54
+ return result;
55
+ }
56
+ catch (error) {
57
+ const diagnostic = error;
58
+ safeDiagnostic(deps, `${stage}.failed`, {
59
+ elapsedMs: Date.now() - startedAt,
60
+ errorCode: typeof diagnostic?.code === "string" ? diagnostic.code : "command_failed",
61
+ httpStatus: typeof diagnostic?.status === "number" ? diagnostic.status : undefined
62
+ });
63
+ throw error;
64
+ }
65
+ }
39
66
  function success(value) {
40
67
  return {
41
68
  schemaVersion: CODEX_DISTRIBUTION_SCHEMA_VERSION,
@@ -216,12 +243,113 @@ function formatV6UserBinding(binding) {
216
243
  + ` (${binding.installationId}) token-set=${binding.credentialLineageId ?? "WAITING"}`;
217
244
  }
218
245
  async function v6DeviceAccess(deps, options = {}, deviceLabel) {
219
- return reconcileCodexV6DeviceAccess({
246
+ return withSafeDiagnostic(deps, "v6.device_access.reconcile", () => reconcileCodexV6DeviceAccess({
220
247
  client: deps.clientFor(serviceConnection(options)),
221
248
  tenantId: deps.config.connection.tenantId,
222
249
  env: deps.env,
223
250
  deviceLabel
251
+ }));
252
+ }
253
+ async function loadV6DeviceCandidates(deps, options = {}, onlineHealth) {
254
+ const client = deps.clientFor(serviceConnection(options));
255
+ const installationId = await getOrCreateCodexInstallationId(deps.env);
256
+ const [requests, directory, localBinding] = await Promise.all([
257
+ client.listV6AccountRequests(),
258
+ client.listAccounts(),
259
+ readCodexV6BindingState(deps.env)
260
+ ]);
261
+ const bindingLists = await Promise.all(requests.items.map(async (request) => ({
262
+ request,
263
+ bindings: (await client.listV6DeviceBindings(request.id)).items
264
+ })));
265
+ const allBindings = bindingLists.flatMap(({ bindings }) => bindings);
266
+ const replaced = new Set(allBindings
267
+ .filter((binding) => binding.predecessorBindingId !== null
268
+ && ["READY", "ACTIVE"].includes(binding.status))
269
+ .map((binding) => binding.predecessorBindingId));
270
+ const now = Date.now();
271
+ const candidates = [];
272
+ for (const { request, bindings } of bindingLists) {
273
+ for (const binding of bindings) {
274
+ if (binding.installationId !== installationId
275
+ || replaced.has(binding.id)
276
+ || !["READY", "ACTIVE", "REPLACEMENT_PENDING", "NEEDS_REAUTH"].includes(binding.status)
277
+ || binding.managedAccountId === null || binding.providerAccountId === null
278
+ || binding.credentialLineageId === null)
279
+ continue;
280
+ const directoryAccount = directory.accounts.find((account) => account.id === binding.managedAccountId || account.accountId === binding.providerAccountId);
281
+ const credentialExpired = binding.credentialExpiresAt === null
282
+ || !Number.isFinite(Date.parse(binding.credentialExpiresAt))
283
+ || Date.parse(binding.credentialExpiresAt) <= now + 60_000;
284
+ const serverRejected = binding.status === "NEEDS_REAUTH"
285
+ || binding.accessTokenHealth === "REJECTED"
286
+ || binding.refreshTokenHealth === "REJECTED";
287
+ const currentRejected = localBinding?.bindingId === binding.id
288
+ && onlineHealth !== undefined
289
+ && ["TOKEN_REVOKED", "AUTH_REJECTED", "CONNECTOR_TOKEN_REVOKED",
290
+ "CONNECTOR_AUTH_REJECTED", "SUBSCRIPTION_FREE"].includes(onlineHealth.status);
291
+ const requestUsable = request.status === "APPROVED"
292
+ && ["AVAILABLE", "PARTIALLY_AVAILABLE"].includes(request.availability)
293
+ && request.validUntil !== null && Date.parse(request.validUntil) > now;
294
+ const available = requestUsable
295
+ && !credentialExpired && !serverRejected && !currentRejected;
296
+ candidates.push({
297
+ request,
298
+ binding,
299
+ account: {
300
+ ...(directoryAccount ?? {
301
+ id: binding.managedAccountId,
302
+ accountId: binding.providerAccountId,
303
+ label: binding.providerAccountId,
304
+ isDefault: false
305
+ }),
306
+ // A binding is the selectable identity. The same managed account may
307
+ // legitimately appear under more than one active Request.
308
+ id: binding.id,
309
+ accountId: binding.providerAccountId,
310
+ isDefault: localBinding?.bindingId === binding.id,
311
+ credentialStatus: available ? "AVAILABLE"
312
+ : credentialExpired || serverRejected || currentRejected
313
+ ? "NEEDS_REAUTH" : "TEMPORARILY_UNAVAILABLE",
314
+ lineageStatus: available ? "ACTIVE" : "NEEDS_REAUTH"
315
+ }
316
+ });
317
+ }
318
+ }
319
+ return {
320
+ installationId,
321
+ candidates,
322
+ ...(localBinding ? { currentBindingId: localBinding.bindingId } : {})
323
+ };
324
+ }
325
+ async function selectV6DeviceCandidate(deps, requested, options = {}) {
326
+ const inventory = await loadV6DeviceCandidates(deps, options);
327
+ let resolvedRequest = requested;
328
+ if (requested) {
329
+ const matches = inventory.candidates.filter(({ request, binding, account }) => [
330
+ request.id,
331
+ binding.id,
332
+ binding.managedAccountId,
333
+ binding.providerAccountId,
334
+ binding.credentialLineageId,
335
+ account.label
336
+ ].includes(requested));
337
+ if (matches.length > 1)
338
+ throw new CodexServiceError(`账号选择 ${requested} 不唯一,请使用 Request ID 或 Binding ID。`, "codex_account_reference_ambiguous", 409);
339
+ if (matches.length === 1)
340
+ resolvedRequest = matches[0].binding.id;
341
+ }
342
+ const chosen = await selectCodexAccount(inventory.candidates.map(({ account }) => account), {
343
+ requested: resolvedRequest,
344
+ selectedAccountId: inventory.currentBindingId,
345
+ nonInteractiveCommand: "numa codex use",
346
+ noAvailableMessage: "当前设备没有可领取或可用的独立 Token Set。",
347
+ output: deps.errorOutput
224
348
  });
349
+ const candidate = inventory.candidates.find(({ binding }) => binding.id === chosen.id);
350
+ if (!candidate)
351
+ throw new CodexServiceError("选择的 Request/设备 Binding 已不在候选列表中。", "codex_device_binding_not_found", 404);
352
+ return candidate;
225
353
  }
226
354
  async function loadAccountData(deps, options = {}, allowV4Replacement = false) {
227
355
  const previous = await loadCodexSelection(deps.config.persistence.configDirectory);
@@ -409,32 +537,82 @@ async function installCredentials(deps, selection, service, allowV4Replacement =
409
537
  async function selectedCredentials(deps, requested, options = {}) {
410
538
  if (requestBoundDeviceCredentialsEnabled(deps.config)) {
411
539
  const client = deps.clientFor(serviceConnection(options));
412
- const ensured = await ensureCodexV6DeviceCredentials({
540
+ const candidate = requested
541
+ ? await selectV6DeviceCandidate(deps, requested, options)
542
+ : undefined;
543
+ let ensured = await withSafeDiagnostic(deps, "v6.device_credentials.ensure", () => ensureCodexV6DeviceCredentials({
413
544
  client,
414
545
  tenantId: deps.config.connection.tenantId,
415
- env: deps.env
416
- });
546
+ env: deps.env,
547
+ ...(candidate ? { target: { request: candidate.request, binding: candidate.binding } } : {})
548
+ }));
417
549
  if (ensured.replacementDeferredWarningCode) {
418
550
  deps.errorOutput.write(`新设备 Token Set 暂时无法交付 (${ensured.replacementDeferredWarningCode});`
419
551
  + "本次继续使用尚有效的旧 Token Set,稍后会自动重试。\n");
420
552
  }
421
- const binding = ensured.binding;
422
- if (requested && ![
423
- binding.id,
424
- binding.requestId,
425
- binding.managedAccountId,
426
- binding.providerAccountId,
427
- binding.credentialLineageId
428
- ].includes(requested)) {
429
- throw new CodexServiceError("当前设备只能使用管理员为它绑定的独立 Token Set。", "codex_device_binding_not_found", 404);
553
+ let binding = ensured.binding;
554
+ let health = await withSafeDiagnostic(deps, "v6.online_health.probe", () => probeCodexV6OnlineCredentialHealth({
555
+ env: deps.env,
556
+ fetch: deps.fetch
557
+ }));
558
+ const healthRejected = () => ["TOKEN_REVOKED", "AUTH_REJECTED", "CONNECTOR_TOKEN_REVOKED",
559
+ "CONNECTOR_AUTH_REJECTED"].includes(health.status);
560
+ const subscriptionUnavailable = () => health.status === "SUBSCRIPTION_FREE";
561
+ let report;
562
+ if (healthRejected()) {
563
+ report = await reportCodexV6OnlineCredentialHealth({ client, health, env: deps.env });
564
+ if (report.replacementBinding) {
565
+ const replacementBinding = report.replacementBinding;
566
+ const request = (await client.getV6AccountRequest(replacementBinding.requestId)).request;
567
+ ensured = await withSafeDiagnostic(deps, "v6.device_credentials.replace", () => ensureCodexV6DeviceCredentials({
568
+ client,
569
+ tenantId: deps.config.connection.tenantId,
570
+ env: deps.env,
571
+ target: { request, binding: replacementBinding }
572
+ }));
573
+ binding = ensured.binding;
574
+ health = await withSafeDiagnostic(deps, "v6.online_health.replacement_probe", () => probeCodexV6OnlineCredentialHealth({ env: deps.env, fetch: deps.fetch }));
575
+ if (!healthRejected()) {
576
+ deps.errorOutput.write(health.status === "HEALTHY"
577
+ ? "已自动安装并验证服务端换发的新设备 Token Set。\n"
578
+ : "已自动安装服务端换发的新设备 Token Set;在线探知结果暂不确定。\n");
579
+ }
580
+ else {
581
+ report = await reportCodexV6OnlineCredentialHealth({ client, health, env: deps.env });
582
+ }
583
+ }
584
+ }
585
+ if (healthRejected()) {
586
+ const connectorOnly = health.status.startsWith("CONNECTOR_");
587
+ throw new CodexServiceError(`当前设备 Token Set 的${connectorOnly ? "关键 ChatGPT/connector 授权" : "主 Codex 授权"}`
588
+ + `已被上游拒绝${health.safeErrorCode ? ` (${health.safeErrorCode})` : ""};`
589
+ + (report?.reported
590
+ ? "失效状态已安全上报,系统将进入换发流程。"
591
+ : `状态上报未完成${report?.warningCode ? ` (${report.warningCode}`
592
+ + `${report.warningStatus ? `/HTTP-${report.warningStatus}` : ""})` : ""};`)
593
+ + "服务器上的“已交付”不代表实时可用,请由管理员换发 Token Set。", ["TOKEN_REVOKED", "CONNECTOR_TOKEN_REVOKED"].includes(health.status)
594
+ ? "codex_device_token_revoked" : "codex_device_credential_rejected", (connectorOnly ? health.connectorHttpStatus : health.codexApiHttpStatus) ?? 401);
430
595
  }
596
+ if (subscriptionUnavailable()) {
597
+ throw new CodexServiceError(`当前 ChatGPT 账号为 Free 套餐${health.planType ? ` (${health.planType})` : ""},无法用于 Numa;请由管理员更换可用的付费账号。`, "codex_device_subscription_free", 403);
598
+ }
599
+ if (["FORBIDDEN", "UNREACHABLE"].includes(health.status)) {
600
+ deps.errorOutput.write(`Codex 在线凭据探知暂不可用 (${health.status.toLowerCase()}`
601
+ + `${health.codexApiHttpStatus ? `, HTTP ${health.codexApiHttpStatus}` : ""});本次仅依据本地与服务端证据继续。\n`);
602
+ }
603
+ const previous = await loadCodexSelection(deps.config.persistence.configDirectory);
604
+ const directoryAccount = candidate?.binding.id === binding.id ? candidate.account
605
+ : (await client.listAccounts()).accounts.find((account) => account.id === binding.managedAccountId || account.accountId === binding.providerAccountId);
606
+ const label = directoryAccount?.label
607
+ ?? (previous?.account.bindingId === binding.id ? previous.account.label : undefined)
608
+ ?? binding.providerAccountId;
431
609
  const selection = await saveCodexSelection(deps.config.persistence.configDirectory, {
432
610
  serviceUrl: options.server ?? deps.config.connection.serverUrl,
433
611
  allowInsecurePoc: Boolean(options.insecurePoc || options.insecureHttp || deps.config.connection.allowInsecureHttp),
434
612
  account: {
435
613
  id: binding.managedAccountId,
436
614
  accountId: binding.providerAccountId,
437
- label: `${binding.deviceLabel} / ${binding.requestId}`,
615
+ label,
438
616
  distributionMode: "REQUEST_BOUND_DEVICE_V6",
439
617
  managedAccountId: binding.managedAccountId,
440
618
  providerAccountId: binding.providerAccountId,
@@ -598,7 +776,11 @@ async function launchWithSelectedCredentials(deps, selected, launch) {
598
776
  env: deps.env
599
777
  });
600
778
  if (!report.reported && report.warningCode) {
601
- deps.errorOutput.write(`Codex 独立设备 Token 代际上报暂缓 (${report.warningCode});其他设备不会受影响。\n`);
779
+ deps.errorOutput.write(`Codex 独立设备 Token 代际上报失败:stage=${report.warningStage ?? "UNKNOWN"}`
780
+ + `${report.warningStatus ? `, HTTP ${report.warningStatus}` : ""}`
781
+ + `, code=${report.warningCode}。`
782
+ + `${report.pendingCredentialChange ? "已检测到本地代际变化,状态与重试证据均已保留;" : ""}`
783
+ + "其他设备不会受影响。\n");
602
784
  }
603
785
  }
604
786
  }
@@ -653,6 +835,27 @@ export function registerCodexCommands(program, deps, options = {}) {
653
835
  ? program
654
836
  : program.command("codex").description("Select an account and launch the official Codex client");
655
837
  const includeAuthCommands = options.includeAuthCommands ?? options.nested === false;
838
+ codex.hook("preAction", (_command, actionCommand) => {
839
+ let serviceOrigin = deps.config.connection.serverUrl;
840
+ try {
841
+ serviceOrigin = new URL(serviceOrigin).origin;
842
+ }
843
+ catch {
844
+ serviceOrigin = "invalid";
845
+ }
846
+ safeDiagnostic(deps, "startup", {
847
+ cliVersion: PACKAGE_VERSION,
848
+ command: actionCommand.name(),
849
+ credentialModel: requestBoundDeviceCredentialsEnabled(deps.config)
850
+ ? "REQUEST_BOUND_DEVICE_V6"
851
+ : "LEGACY",
852
+ profileId: deps.config.profile?.profileId,
853
+ profileRevision: deps.config.profile?.revision,
854
+ environment: deps.config.profile?.environment,
855
+ profileStale: deps.config.profile?.stale,
856
+ serviceOrigin
857
+ });
858
+ });
656
859
  if (includeAuthCommands)
657
860
  codex.command("login").description("Log in with OIDC").action(async (_options, command) => {
658
861
  if (!deps.auth.login)
@@ -776,31 +979,45 @@ export function registerCodexCommands(program, deps, options = {}) {
776
979
  serviceOptions(codex.command("list").description("List available Codex accounts without downloading credentials"))
777
980
  .action(async (opts, command) => {
778
981
  if (requestBoundDeviceCredentialsEnabled(deps.config)) {
779
- const client = deps.clientFor(serviceConnection(opts));
780
982
  const access = await v6DeviceAccess(deps, opts);
983
+ const localHealth = await probeCodexV6OnlineCredentialHealth({
984
+ env: deps.env,
985
+ fetch: deps.fetch
986
+ });
987
+ const rejected = ["TOKEN_REVOKED", "AUTH_REJECTED", "CONNECTOR_TOKEN_REVOKED",
988
+ "CONNECTOR_AUTH_REJECTED"].includes(localHealth.status);
989
+ const healthReport = rejected
990
+ ? await reportCodexV6OnlineCredentialHealth({
991
+ client: deps.clientFor(serviceConnection(opts)),
992
+ health: localHealth,
993
+ env: deps.env
994
+ })
995
+ : undefined;
996
+ const inventory = await loadV6DeviceCandidates(deps, opts, localHealth);
781
997
  if (await json(command, deps)) {
782
998
  print(deps.output, success({
783
999
  credentialModel: "REQUEST_BOUND_DEVICE_V6",
784
1000
  installationId: access.installationId,
785
- ...access.response
1001
+ ...access.response,
1002
+ candidates: inventory.candidates.map(({ request, binding, account }) => ({
1003
+ request,
1004
+ binding,
1005
+ account
1006
+ })),
1007
+ localCredentialHealth: localHealth,
1008
+ ...(healthReport ? { localCredentialHealthReport: healthReport } : {})
786
1009
  }));
787
1010
  return;
788
1011
  }
789
- if (access.response.accessStatus !== "AVAILABLE") {
1012
+ if (inventory.candidates.length === 0 && access.response.accessStatus !== "AVAILABLE") {
790
1013
  requireAvailableCodexV6DeviceAccess(access);
791
1014
  }
792
- const { binding } = requireAvailableCodexV6DeviceAccess(access);
793
- const accountResult = await client.listAccounts();
794
- const currentAccount = accountResult.accounts.find((account) => account.id === binding.managedAccountId || account.accountId === binding.providerAccountId);
795
- if (!currentAccount)
796
- throw new CodexServiceError("The request-bound ChatGPT account is absent from the user account directory.", "invalid_codex_service_response");
797
- const availableAccount = {
798
- ...currentAccount,
799
- credentialStatus: "AVAILABLE",
800
- lineageStatus: "ACTIVE"
801
- };
802
- deps.output.write(renderCodexAccountDashboard([availableAccount], availableAccount.id));
803
- deps.output.write(`设备凭据 ${binding.status === "ACTIVE" || binding.status === "READY" ? "可用" : binding.status}\n`);
1015
+ deps.output.write(renderCodexAccountDashboard(inventory.candidates.map(({ account }) => account), inventory.currentBindingId ?? access.response.binding?.id ?? undefined));
1016
+ deps.output.write(`设备凭据 ${rejected ? "已失效,需换发" : "可用"}\n`);
1017
+ if (healthReport?.reported)
1018
+ deps.output.write(healthReport.replacementPending
1019
+ ? "失效状态已上报;同账号 Token Set 暂无库存,正在等待换发。\n"
1020
+ : "失效状态已上报;新的设备 Token Set 将在使用时自动换发。\n");
804
1021
  if (access.response.migration === "AUTO_MIGRATED") {
805
1022
  deps.output.write("既有本机凭据已自动转换为该申请下的第一个独立设备绑定。\n");
806
1023
  }
@@ -831,7 +1048,8 @@ export function registerCodexCommands(program, deps, options = {}) {
831
1048
  if (requestBoundDeviceCredentialsEnabled(deps.config)) {
832
1049
  if (opts.takeover)
833
1050
  throw new CodexServiceError("v6 每台设备使用独立 Token Set,不支持接管其他设备的凭据。", "codex_device_takeover_not_allowed", 409);
834
- const selected = await selectedCredentials(deps, account, opts);
1051
+ const target = account ?? (await selectV6DeviceCandidate(deps, undefined, opts)).binding.id;
1052
+ const selected = await selectedCredentials(deps, target, opts);
835
1053
  if (await json(command, deps))
836
1054
  print(deps.output, success({
837
1055
  account: selected.selection.account,
@@ -862,7 +1080,10 @@ export function registerCodexCommands(program, deps, options = {}) {
862
1080
  .action(async (args, opts) => {
863
1081
  if (opts.selectAccount)
864
1082
  await clearCodexSelection(deps.config.persistence.configDirectory);
865
- const selected = await selectedCredentials(deps, opts.account, opts);
1083
+ const target = requestBoundDeviceCredentialsEnabled(deps.config) && opts.selectAccount
1084
+ ? (await selectV6DeviceCandidate(deps, opts.account, opts)).binding.id
1085
+ : opts.account;
1086
+ const selected = await selectedCredentials(deps, target, opts);
866
1087
  deps.errorOutput.write(`Using ChatGPT account: ${selected.selection.account.label} (${selected.selection.account.id})\n`);
867
1088
  process.exitCode = await launchWithSelectedCredentials(deps, selected, (signal) => deps.run(args, { signal }));
868
1089
  });
@@ -874,7 +1095,10 @@ export function registerCodexCommands(program, deps, options = {}) {
874
1095
  throw new Error("官方 Codex 桌面 App 仅支持 macOS 和 Windows。");
875
1096
  if (opts.selectAccount)
876
1097
  await clearCodexSelection(deps.config.persistence.configDirectory);
877
- const selected = await selectedCredentials(deps, opts.account, opts);
1098
+ const target = requestBoundDeviceCredentialsEnabled(deps.config) && opts.selectAccount
1099
+ ? (await selectV6DeviceCandidate(deps, opts.account, opts)).binding.id
1100
+ : opts.account;
1101
+ const selected = await selectedCredentials(deps, target, opts);
878
1102
  deps.errorOutput.write(`Using ChatGPT account: ${selected.selection.account.label} (${selected.selection.account.id})\n`);
879
1103
  process.exitCode = await launchWithSelectedCredentials(deps, selected, (signal) => deps.app(workspace, { signal }));
880
1104
  });
@@ -976,13 +1200,18 @@ export function registerCodexCommands(program, deps, options = {}) {
976
1200
  ? "device_token_set.generation_reported"
977
1201
  : "device_token_set.current",
978
1202
  at: new Date().toISOString(),
979
- ...(result.warningCode ? { warningCode: result.warningCode } : {})
1203
+ ...(result.warningCode ? { warningCode: result.warningCode } : {}),
1204
+ ...(result.warningStage ? { warningStage: result.warningStage } : {}),
1205
+ ...(result.warningStatus ? { warningStatus: result.warningStatus } : {}),
1206
+ ...(result.pendingCredentialChange ? { pendingCredentialChange: true } : {})
980
1207
  };
981
1208
  if (await json(command, deps))
982
1209
  print(deps.output, success(event));
983
1210
  else
984
1211
  deps.output.write(`${event.at} ${event.level} ${event.event}`
985
- + `${result.warningCode ? ` (${result.warningCode})` : ""}; other devices unaffected\n`);
1212
+ + `${result.warningCode ? ` (${result.warningStage ?? "UNKNOWN"}`
1213
+ + `${result.warningStatus ? `/HTTP-${result.warningStatus}` : ""}`
1214
+ + `/${result.warningCode})` : ""}; other devices unaffected\n`);
986
1215
  if (opts.once)
987
1216
  break;
988
1217
  await new Promise((resolve) => setTimeout(resolve, interval));