@yansigit/opencodex 2.36.1-dev.20260829.48 → 2.36.1
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/gui/dist/assets/{ApiKeys-LOOFiZfv.js → ApiKeys-CsmNyf4I.js} +1 -1
- package/gui/dist/assets/{Claude-BwKzpXe3.js → Claude-B4sHsGTD.js} +1 -1
- package/gui/dist/assets/{CodexSet-B5qi9KeE.js → CodexSet-BuAQ4YQn.js} +1 -1
- package/gui/dist/assets/{FileIntegrationPage-B0sUef6W.js → FileIntegrationPage-BC65sEaS.js} +1 -1
- package/gui/dist/assets/{Grok-bsb4n-f4.js → Grok-DNJWlISf.js} +1 -1
- package/gui/dist/assets/{Integrations-CSMwFDvM.js → Integrations-vGJshFZK.js} +2 -2
- package/gui/dist/assets/{IntegrationsOverview-CTORdKJA.js → IntegrationsOverview-DYMkDFHE.js} +1 -1
- package/gui/dist/assets/{Logs-BWYbfELf.js → Logs-Coc7_RPP.js} +1 -1
- package/gui/dist/assets/{Models-CexEtdT1.js → Models-CKVT8Meb.js} +1 -1
- package/gui/dist/assets/{NumberStepper-BapSFQnW.js → NumberStepper-Dug4wR8n.js} +1 -1
- package/gui/dist/assets/{Providers-BwUFeAgA.js → Providers-DyaCHRWL.js} +1 -1
- package/gui/dist/assets/{RestoreDialog-CD8piq90.js → RestoreDialog-BpuvVU2w.js} +1 -1
- package/gui/dist/assets/{Startup-BgX731C2.js → Startup-DAUQqNEN.js} +1 -1
- package/gui/dist/assets/{Storage-Bgq7HphP.js → Storage-DfP95SlJ.js} +1 -1
- package/gui/dist/assets/{Subagents-ChxItbeF.js → Subagents-CMNsZLfk.js} +1 -1
- package/gui/dist/assets/{Usage-BkylzP50.js → Usage-ESFRfBMO.js} +1 -1
- package/gui/dist/assets/{codex-stale-banner-D4gvSWnO.js → codex-stale-banner-W4FhikN7.js} +1 -1
- package/gui/dist/assets/{data-surface-BtO3lwam.js → data-surface-B8PX8rjm.js} +1 -1
- package/gui/dist/assets/{data-surface-pCXChiBf.js → data-surface-CETFXqiA.js} +1 -1
- package/gui/dist/assets/{index-CNopOid3.js → index-DHQHBRhb.js} +3 -3
- package/gui/dist/assets/{model-display-CiUpg8T9.js → model-display-OBTJhQwT.js} +1 -1
- package/gui/dist/assets/{provider-payload-b2jlS-On.js → provider-payload-CJRJUn7w.js} +1 -1
- package/gui/dist/assets/{section-tabs-BP7gEPK6.js → section-tabs-B8sLMiz6.js} +1 -1
- package/gui/dist/assets/shared-DI18-uF7.js +69 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/adapters/base.ts +26 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +34 -41
- package/src/adapters/cursor/envelope-echo.ts +128 -0
- package/src/adapters/cursor/request-builder.ts +19 -12
- package/src/adapters/cursor/tool-definitions.ts +2 -1
- package/src/adapters/cursor/tool-result-normalize.ts +23 -31
- package/src/adapters/cursor.ts +21 -2
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +71 -2
- package/src/adapters/google.ts +19 -4
- package/src/adapters/kiro-constants.ts +12 -0
- package/src/adapters/kiro.ts +128 -11
- package/src/adapters/openai-chat.ts +16 -2
- package/src/adapters/openai-responses.ts +15 -2
- package/src/adapters/tool-catalog-nudge.ts +2 -1
- package/src/adapters/xai-web-search.ts +10 -14
- package/src/claude/outbound.ts +14 -3
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +93 -16
- package/src/cli/account-extended.ts +262 -36
- package/src/cli/account-main.ts +12 -12
- package/src/cli/account.ts +40 -10
- package/src/cli/agent.ts +8 -1
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +535 -0
- package/src/cli/claude-desktop.ts +31 -11
- package/src/cli/dispatch.ts +195 -27
- package/src/cli/doctor.ts +100 -1
- package/src/cli/help.ts +11 -2
- package/src/cli/index.ts +19 -3
- package/src/cli/inspect.ts +230 -0
- package/src/cli/observe.ts +11 -3
- package/src/cli/registry.ts +34 -2
- package/src/cli/runtime-api.ts +51 -7
- package/src/cli/status.ts +16 -0
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/usage-report.ts +52 -2
- package/src/cli/version-skew.ts +46 -0
- package/src/codex/account-label.ts +21 -0
- package/src/codex/catalog/provider-fetch.ts +4 -0
- package/src/codex/transition-state.ts +12 -3
- package/src/compatibility/openai-responses.ts +9 -1
- package/src/generated/compatibility-version.json +94 -58
- package/src/integrations/ownership-policy.ts +24 -5
- package/src/integrations/ownership.ts +36 -2
- package/src/integrations/state.ts +40 -7
- package/src/integrations/writer.ts +21 -3
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/errors.ts +25 -1
- package/src/lib/service-secrets.ts +15 -0
- package/src/oauth/store.ts +16 -6
- package/src/providers/label.ts +34 -1
- package/src/responses/turn-termination.ts +107 -0
- package/src/server/management/logs-usage-routes.ts +5 -27
- package/src/server/management/route-registry.ts +317 -0
- package/src/server/proxy-liveness.ts +27 -4
- package/src/server/request-log.ts +37 -17
- package/src/server/responses/core.ts +80 -4
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/service.ts +34 -0
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +79 -33
- package/src/usage/log.ts +32 -6
- package/src/usage/summary.ts +22 -34
- package/gui/dist/assets/shared-BdYGV-yJ.js +0 -69
package/src/cli/account-api.ts
CHANGED
|
@@ -24,6 +24,14 @@ export interface AccountRow {
|
|
|
24
24
|
/** Codex pool selection order, higher used earlier. Absent where ordering does not apply. */
|
|
25
25
|
priority?: number;
|
|
26
26
|
quota?: CodexQuotaDto | null;
|
|
27
|
+
/**
|
|
28
|
+
* Whether the pool is holding this account out of rotation.
|
|
29
|
+
*
|
|
30
|
+
* The server has always sent it (auth-api.ts:286 for pool accounts, :1315 for main) and the
|
|
31
|
+
* CLI dropped it, so a paused account was indistinguishable from an available one in every
|
|
32
|
+
* human listing (#2703).
|
|
33
|
+
*/
|
|
34
|
+
paused?: boolean;
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
export type ClassifyResult = { type: AccountType } | { error: string };
|
|
@@ -83,6 +91,12 @@ export interface ApiResult {
|
|
|
83
91
|
/** 0 = network-level failure (proxy unreachable). */
|
|
84
92
|
status: number;
|
|
85
93
|
json: Record<string, unknown>;
|
|
94
|
+
/**
|
|
95
|
+
* Message from the thrown transport error when `status` is 0. Previously the
|
|
96
|
+
* error was swallowed by a catch block with an empty body, so an unreachable proxy, a DNS
|
|
97
|
+
* failure and a TLS error were indistinguishable (#2698).
|
|
98
|
+
*/
|
|
99
|
+
transportError?: string;
|
|
86
100
|
}
|
|
87
101
|
|
|
88
102
|
export async function apiJson(
|
|
@@ -103,8 +117,14 @@ export async function apiJson(
|
|
|
103
117
|
});
|
|
104
118
|
const json = (await res.json().catch(() => ({}))) as Record<string, unknown>;
|
|
105
119
|
return { status: res.status, json };
|
|
106
|
-
} catch {
|
|
107
|
-
|
|
120
|
+
} catch (error) {
|
|
121
|
+
// status 0 stays the transport sentinel, but keep the cause: callers can now
|
|
122
|
+
// tell the operator why the request never reached the proxy (#2698).
|
|
123
|
+
return {
|
|
124
|
+
status: 0,
|
|
125
|
+
json: {},
|
|
126
|
+
transportError: error instanceof Error ? error.message : String(error),
|
|
127
|
+
};
|
|
108
128
|
}
|
|
109
129
|
}
|
|
110
130
|
|
|
@@ -115,18 +135,43 @@ export async function resolveBaseUrl(deps: AccountDeps): Promise<string | null>
|
|
|
115
135
|
return `http://${probeHostname(live.hostname)}:${live.port}`;
|
|
116
136
|
}
|
|
117
137
|
|
|
118
|
-
export function proxyUnreachable(): number {
|
|
138
|
+
export function proxyUnreachable(transportError?: string): number {
|
|
119
139
|
console.error("Proxy not reachable. Start it with 'ocx start' or 'ocx ensure'.");
|
|
140
|
+
// Naming the transport cause distinguishes "nothing is listening" from a refused
|
|
141
|
+
// or reset connection, which is what made #2696-class breakage undiagnosable.
|
|
142
|
+
if (transportError) console.error(`reason: ${transportError}`);
|
|
120
143
|
return 1;
|
|
121
144
|
}
|
|
122
145
|
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
146
|
+
function accountStringField(json: Record<string, unknown>, key: string): string | undefined {
|
|
147
|
+
const value = json[key];
|
|
148
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Report a failed management call from the account family.
|
|
153
|
+
*
|
|
154
|
+
* `reason` and `hint` are the actionable fields on a refusal — the management plane
|
|
155
|
+
* sets both on a 503, and several routes return `reason` with no `error` key at all,
|
|
156
|
+
* which used to print only the generic fallback (#2698).
|
|
157
|
+
*
|
|
158
|
+
* `status` selects the exit code so the account client speaks the same vocabulary as
|
|
159
|
+
* runtime-api.ts: 4 for not-found, 5 for conflict, 1 otherwise. Previously every
|
|
160
|
+
* failure exited 1, so a script could not distinguish a missing account from a
|
|
161
|
+
* concurrent mutation.
|
|
162
|
+
*/
|
|
163
|
+
export function apiError(json: Record<string, unknown>, fallback: string, status: number): number {
|
|
164
|
+
const primary = accountStringField(json, "error") ?? fallback;
|
|
165
|
+
const lines = [`Error: ${primary}`];
|
|
166
|
+
const reason = accountStringField(json, "reason");
|
|
167
|
+
if (reason && reason !== primary) lines.push(`reason: ${reason}`);
|
|
168
|
+
const hint = accountStringField(json, "hint");
|
|
169
|
+
if (hint && hint !== primary) lines.push(`hint: ${hint}`);
|
|
170
|
+
for (const line of lines) console.error(line);
|
|
126
171
|
if (json.cleanupRequired === true) {
|
|
127
172
|
console.error("Warning: native-login staging cleanup is still required; run 'ocx account main doctor'.");
|
|
128
173
|
}
|
|
129
|
-
return 1;
|
|
174
|
+
return status === 404 ? 4 : status === 409 ? 5 : 1;
|
|
130
175
|
}
|
|
131
176
|
|
|
132
177
|
export interface FamilyRows {
|
|
@@ -134,19 +179,27 @@ export interface FamilyRows {
|
|
|
134
179
|
activeId: string | null;
|
|
135
180
|
autoSwitchThreshold?: number;
|
|
136
181
|
/** HTTP status for a completed family read, including failures. */
|
|
137
|
-
status
|
|
182
|
+
status: number;
|
|
138
183
|
/** Set when the family endpoint returned an error. */
|
|
139
184
|
errorJson?: Record<string, unknown>;
|
|
140
185
|
networkDown?: boolean;
|
|
186
|
+
/** Transport cause when `networkDown` is set. Callers must forward this to `proxyUnreachable`. */
|
|
187
|
+
transportError?: string;
|
|
141
188
|
}
|
|
142
189
|
|
|
143
190
|
export interface CodexQuotaDto {
|
|
144
|
-
fiveHourPercent?: number;
|
|
145
|
-
fiveHourResetAt?: number;
|
|
146
191
|
weeklyPercent?: number;
|
|
147
192
|
monthlyPercent?: number;
|
|
148
193
|
weeklyResetAt?: number;
|
|
149
194
|
monthlyResetAt?: number;
|
|
195
|
+
/**
|
|
196
|
+
* Five-hour window, as the per-account provider probe reports it
|
|
197
|
+
* (`/api/oauth/accounts?quota=1`). Distinct from `shortPercent`, which is the Codex pool's
|
|
198
|
+
* self-declared burst window; the two surfaces name the same idea differently and both reach
|
|
199
|
+
* this DTO.
|
|
200
|
+
*/
|
|
201
|
+
fiveHourPercent?: number;
|
|
202
|
+
fiveHourResetAt?: number;
|
|
150
203
|
/** Sub-day burst window, when upstream declares one (#1791). */
|
|
151
204
|
shortPercent?: number;
|
|
152
205
|
shortResetAt?: number;
|
|
@@ -160,6 +213,8 @@ export interface ProviderQuotaWindowDto {
|
|
|
160
213
|
}
|
|
161
214
|
|
|
162
215
|
export interface ProviderQuotaDto extends CodexQuotaDto {
|
|
216
|
+
fiveHourPercent?: number;
|
|
217
|
+
fiveHourResetAt?: number;
|
|
163
218
|
customWindows?: ProviderQuotaWindowDto[];
|
|
164
219
|
updatedAt?: number;
|
|
165
220
|
}
|
|
@@ -182,11 +237,18 @@ interface CodexAccountDto {
|
|
|
182
237
|
needsReauth?: boolean;
|
|
183
238
|
priority?: number;
|
|
184
239
|
quota?: CodexQuotaDto | null;
|
|
240
|
+
paused?: boolean;
|
|
185
241
|
}
|
|
186
242
|
|
|
187
243
|
function projectQuota(quota: CodexQuotaDto | null | undefined): CodexQuotaDto | null {
|
|
188
244
|
if (!quota) return null;
|
|
189
245
|
const projected: CodexQuotaDto = {};
|
|
246
|
+
// `fiveHourPercent`/`fiveHourResetAt` were declared on the DTO and read by two renderers
|
|
247
|
+
// -- `quotaText`'s `quota.fiveHourPercent ?? quota.shortPercent` (account.ts:89) and
|
|
248
|
+
// `quotaParts` (account-extended.ts:275) -- but omitted from this whitelist, so the first
|
|
249
|
+
// operand was unreachable and a 5h-only account rendered as unknown (#2703). A projection
|
|
250
|
+
// that silently drops a field its own type declares is worse than one that never had it:
|
|
251
|
+
// the type checks, the renderer looks correct, and only the output is wrong.
|
|
190
252
|
for (const key of ["fiveHourPercent", "fiveHourResetAt", "weeklyPercent", "monthlyPercent", "weeklyResetAt", "monthlyResetAt", "shortPercent", "shortResetAt", "shortWindowSeconds"] as const) {
|
|
191
253
|
if (typeof quota[key] === "number" && Number.isFinite(quota[key])) projected[key] = quota[key];
|
|
192
254
|
}
|
|
@@ -197,6 +259,7 @@ export async function fetchCodexRows(
|
|
|
197
259
|
deps: AccountDeps,
|
|
198
260
|
baseUrl: string,
|
|
199
261
|
forceRefresh = false,
|
|
262
|
+
includeQuota = forceRefresh,
|
|
200
263
|
): Promise<FamilyRows> {
|
|
201
264
|
const accountsPath = `/api/codex-auth/accounts${forceRefresh ? "?refresh=1" : ""}`;
|
|
202
265
|
const [accountsRes, activeRes] = await Promise.all([
|
|
@@ -210,7 +273,13 @@ export async function fetchCodexRows(
|
|
|
210
273
|
return { rows: [], activeId: null, status: activeRes.status, errorJson: activeRes.json };
|
|
211
274
|
}
|
|
212
275
|
if (accountsRes.status === 0 || activeRes.status === 0) {
|
|
213
|
-
return {
|
|
276
|
+
return {
|
|
277
|
+
rows: [],
|
|
278
|
+
activeId: null,
|
|
279
|
+
status: 0,
|
|
280
|
+
networkDown: true,
|
|
281
|
+
transportError: accountsRes.transportError ?? activeRes.transportError,
|
|
282
|
+
};
|
|
214
283
|
}
|
|
215
284
|
const activeId = typeof activeRes.json.activeCodexAccountId === "string"
|
|
216
285
|
? activeRes.json.activeCodexAccountId
|
|
@@ -229,7 +298,8 @@ export async function fetchCodexRows(
|
|
|
229
298
|
active: a.id === activeId,
|
|
230
299
|
needsReauth: a.needsReauth,
|
|
231
300
|
priority: typeof a.priority === "number" ? a.priority : 0,
|
|
232
|
-
|
|
301
|
+
paused: a.paused === true,
|
|
302
|
+
...(includeQuota ? { quota: projectQuota(a.quota) } : {}),
|
|
233
303
|
}));
|
|
234
304
|
return { rows, activeId, autoSwitchThreshold, status: 200 };
|
|
235
305
|
}
|
|
@@ -256,7 +326,9 @@ async function fetchOAuthRows(
|
|
|
256
326
|
? `?provider=${encodeURIComponent(name)}"a=1${quota.refresh ? "&refresh=1" : ""}`
|
|
257
327
|
: `?provider=${encodeURIComponent(name)}`;
|
|
258
328
|
const res = await apiJson(deps, baseUrl, "GET", `/api/oauth/accounts${query}`);
|
|
259
|
-
if (res.status === 0)
|
|
329
|
+
if (res.status === 0) {
|
|
330
|
+
return { rows: [], activeId: null, status: 0, networkDown: true, transportError: res.transportError };
|
|
331
|
+
}
|
|
260
332
|
if (res.status !== 200) return { rows: [], activeId: null, status: res.status, errorJson: res.json };
|
|
261
333
|
const activeId = typeof res.json.activeAccountId === "string" ? res.json.activeAccountId : null;
|
|
262
334
|
const accounts = Array.isArray(res.json.accounts) ? res.json.accounts as OAuthAccountDto[] : [];
|
|
@@ -283,7 +355,9 @@ interface ApiKeyDto {
|
|
|
283
355
|
|
|
284
356
|
async function fetchKeyRows(deps: AccountDeps, baseUrl: string, name: string): Promise<FamilyRows> {
|
|
285
357
|
const res = await apiJson(deps, baseUrl, "GET", `/api/providers/keys?name=${encodeURIComponent(name)}`);
|
|
286
|
-
if (res.status === 0)
|
|
358
|
+
if (res.status === 0) {
|
|
359
|
+
return { rows: [], activeId: null, status: 0, networkDown: true, transportError: res.transportError };
|
|
360
|
+
}
|
|
287
361
|
if (res.status !== 200) return { rows: [], activeId: null, status: res.status, errorJson: res.json };
|
|
288
362
|
const activeId = typeof res.json.activeId === "string" ? res.json.activeId : null;
|
|
289
363
|
const keys = Array.isArray(res.json.keys) ? res.json.keys as ApiKeyDto[] : [];
|
|
@@ -305,7 +379,7 @@ export function fetchRows(
|
|
|
305
379
|
type: AccountType,
|
|
306
380
|
quota?: { refresh?: boolean },
|
|
307
381
|
): Promise<FamilyRows> {
|
|
308
|
-
if (type === "codex") return fetchCodexRows(deps, baseUrl);
|
|
382
|
+
if (type === "codex") return fetchCodexRows(deps, baseUrl, Boolean(quota?.refresh), quota !== undefined);
|
|
309
383
|
if (type === "oauth") return fetchOAuthRows(deps, baseUrl, name, quota);
|
|
310
384
|
return fetchKeyRows(deps, baseUrl, name);
|
|
311
385
|
}
|
|
@@ -314,8 +388,11 @@ export async function fetchProviderQuotaReport(
|
|
|
314
388
|
deps: AccountDeps,
|
|
315
389
|
baseUrl: string,
|
|
316
390
|
name: string,
|
|
317
|
-
): Promise<{ status: number; report: ProviderQuotaReportDto | null; errorJson?: Record<string, unknown
|
|
391
|
+
): Promise<{ status: number; report: ProviderQuotaReportDto | null; errorJson?: Record<string, unknown>; transportError?: string }> {
|
|
318
392
|
const res = await apiJson(deps, baseUrl, "GET", "/api/provider-quotas?refresh=1");
|
|
393
|
+
if (res.status === 0) {
|
|
394
|
+
return { status: 0, report: null, errorJson: res.json, transportError: res.transportError };
|
|
395
|
+
}
|
|
319
396
|
if (res.status !== 200) return { status: res.status, report: null, errorJson: res.json };
|
|
320
397
|
const reports = Array.isArray(res.json.reports) ? res.json.reports as ProviderQuotaReportDto[] : [];
|
|
321
398
|
return { status: 200, report: reports.find(report => report?.provider === name) ?? null };
|
|
@@ -40,6 +40,11 @@ const EXTENDED_USAGE = `Usage:
|
|
|
40
40
|
ocx account auto-switch <provider> <on|off|status|threshold <0-100>> [--json]
|
|
41
41
|
ocx account alias <provider> <id|main> <display-name|-> [--json]
|
|
42
42
|
ocx account priority <provider> <id|main> [<-100..100|first|earlier|normal|later|last|reset>] [--json]
|
|
43
|
+
ocx account pause <provider> <id|main> [--json]
|
|
44
|
+
ocx account resume <provider> <id|main> [--json]
|
|
45
|
+
ocx account pause-exhausted <provider> [--json]
|
|
46
|
+
ocx account strategy <provider> [<quota|round-robin|fill-first>] [--json]
|
|
47
|
+
ocx account sticky <provider> [<1-100>] [--json]
|
|
43
48
|
ocx account remove <provider> <id|main> --yes [--json]
|
|
44
49
|
ocx account clear-cooldown <provider> <id|main> [--json]
|
|
45
50
|
ocx account add-key <provider> [--label <label>] [--json]
|
|
@@ -232,8 +237,8 @@ function configAndType(deps: AccountDeps, name: string) {
|
|
|
232
237
|
}
|
|
233
238
|
|
|
234
239
|
function familyFailure(result: FamilyRows, fallback: string): number | null {
|
|
235
|
-
if (result.networkDown) return proxyUnreachable();
|
|
236
|
-
if (result.errorJson) return apiError(result.errorJson, fallback);
|
|
240
|
+
if (result.networkDown) return proxyUnreachable(result.transportError);
|
|
241
|
+
if (result.errorJson) return apiError(result.errorJson, fallback, result.status);
|
|
237
242
|
return null;
|
|
238
243
|
}
|
|
239
244
|
|
|
@@ -249,24 +254,14 @@ function resetIso(value: number | undefined): string | null {
|
|
|
249
254
|
|
|
250
255
|
function refreshLine(row: FamilyRows["rows"][number]): string {
|
|
251
256
|
const parts = [row.id === MAIN_ID ? "main" : row.id, row.email, row.plan];
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const fiveHourReset = resetIso(fiveHourResetAt);
|
|
261
|
-
if (fiveHourReset) parts.push(`resets ${fiveHourReset}`);
|
|
262
|
-
}
|
|
263
|
-
if (quota.weeklyPercent !== undefined) parts.push(`weekly ${quota.weeklyPercent}%`);
|
|
264
|
-
const weeklyReset = resetIso(quota.weeklyResetAt);
|
|
265
|
-
if (weeklyReset) parts.push(`resets ${weeklyReset}`);
|
|
266
|
-
if (quota.monthlyPercent !== undefined) parts.push(`monthly ${quota.monthlyPercent}%`);
|
|
267
|
-
const monthlyReset = resetIso(quota.monthlyResetAt);
|
|
268
|
-
if (monthlyReset) parts.push(`resets ${monthlyReset}`);
|
|
269
|
-
}
|
|
257
|
+
if (row.paused) parts.push("paused");
|
|
258
|
+
// Was a second quota dialect: it gated the whole block on weekly/monthly, so an account
|
|
259
|
+
// reporting only a 5h window printed `quota: unknown` while `quotaParts` five lines below
|
|
260
|
+
// rendered the same data correctly for the provider path (#2703). Two halves of one file
|
|
261
|
+
// disagreeing about how to read one DTO is the defect; delegating removes it rather than
|
|
262
|
+
// teaching the second dialect a third window.
|
|
263
|
+
const quotaText = row.quota ? quotaParts(row.quota).join(" ") : "";
|
|
264
|
+
parts.push(quotaText.length > 0 ? quotaText : "quota: unknown");
|
|
270
265
|
if (row.needsReauth) parts.push("needs-reauth");
|
|
271
266
|
return parts.filter(Boolean).join(" ");
|
|
272
267
|
}
|
|
@@ -279,7 +274,7 @@ function quotaParts(quota: ProviderQuotaDto): string[] {
|
|
|
279
274
|
const reset = resetIso(resetAt);
|
|
280
275
|
if (reset) parts.push(`resets ${reset}`);
|
|
281
276
|
};
|
|
282
|
-
add("5h", quota.fiveHourPercent
|
|
277
|
+
add("5h", quota.fiveHourPercent, quota.fiveHourResetAt);
|
|
283
278
|
add("weekly", quota.weeklyPercent, quota.weeklyResetAt);
|
|
284
279
|
add("monthly", quota.monthlyPercent, quota.monthlyResetAt);
|
|
285
280
|
for (const window of quota.customWindows ?? []) add(window.label, window.percent, window.resetAt);
|
|
@@ -332,8 +327,8 @@ export async function cmdRefresh(args: string[], deps: AccountDeps): Promise<num
|
|
|
332
327
|
if (!baseUrl) return proxyUnreachable();
|
|
333
328
|
if (classified.type !== "codex") {
|
|
334
329
|
const result = await fetchProviderQuotaReport(deps, baseUrl, name);
|
|
335
|
-
if (result.status === 0) return proxyUnreachable();
|
|
336
|
-
if (result.status !== 200) return apiError(result.errorJson ?? {}, `failed to refresh ${name}
|
|
330
|
+
if (result.status === 0) return proxyUnreachable(result.transportError);
|
|
331
|
+
if (result.status !== 200) return apiError(result.errorJson ?? {}, `failed to refresh ${name}`, result.status);
|
|
337
332
|
if (wantsJson) console.log(JSON.stringify({ provider: name, report: result.report }, null, 2));
|
|
338
333
|
else console.log(result.report ? providerQuotaLine(name, result.report) : `no quota report available for ${name}`);
|
|
339
334
|
return 0;
|
|
@@ -367,15 +362,15 @@ export async function cmdAutoSwitch(args: string[], deps: AccountDeps): Promise<
|
|
|
367
362
|
if (!baseUrl) return proxyUnreachable();
|
|
368
363
|
if (action === "status") {
|
|
369
364
|
const response = await apiJson(deps, baseUrl, "GET", "/api/codex-auth/active");
|
|
370
|
-
if (response.status === 0) return proxyUnreachable();
|
|
365
|
+
if (response.status === 0) return proxyUnreachable(response.transportError);
|
|
371
366
|
if (response.status !== 200 || typeof response.json.autoSwitchThreshold !== "number") {
|
|
372
|
-
return apiError(response.json, "failed to read auto-switch status");
|
|
367
|
+
return apiError(response.json, "failed to read auto-switch status", response.status);
|
|
373
368
|
}
|
|
374
369
|
threshold = response.json.autoSwitchThreshold;
|
|
375
370
|
} else {
|
|
376
371
|
const response = await apiJson(deps, baseUrl, "PUT", "/api/codex-auth/auto-switch", { threshold });
|
|
377
|
-
if (response.status === 0) return proxyUnreachable();
|
|
378
|
-
if (response.status !== 200) return apiError(response.json, "failed to update auto-switch");
|
|
372
|
+
if (response.status === 0) return proxyUnreachable(response.transportError);
|
|
373
|
+
if (response.status !== 200) return apiError(response.json, "failed to update auto-switch", response.status);
|
|
379
374
|
}
|
|
380
375
|
const enabled = threshold! > 0;
|
|
381
376
|
if (wantsJson) console.log(JSON.stringify({ provider: name, autoSwitchThreshold: threshold, enabled }, null, 2));
|
|
@@ -466,8 +461,8 @@ export async function cmdAddKey(args: string[], deps: AccountDeps): Promise<numb
|
|
|
466
461
|
const baseUrl = await resolveBaseUrl(deps);
|
|
467
462
|
if (!baseUrl) return proxyUnreachable();
|
|
468
463
|
const response = await apiJson(deps, baseUrl, "POST", "/api/providers/keys", { name, key, ...(label ? { label } : {}) });
|
|
469
|
-
if (response.status === 0) return proxyUnreachable();
|
|
470
|
-
if (response.status !== 201) return apiError(response.json, `failed to add a key for ${name}
|
|
464
|
+
if (response.status === 0) return proxyUnreachable(response.transportError);
|
|
465
|
+
if (response.status !== 201) return apiError(response.json, `failed to add a key for ${name}`, response.status);
|
|
471
466
|
const id = typeof response.json.id === "string" ? response.json.id : null;
|
|
472
467
|
// Redact the key inside the label BEFORE serialization — a key containing
|
|
473
468
|
// JSON-escaped characters (" or \) would otherwise survive the whole-output
|
|
@@ -543,7 +538,7 @@ export async function cmdImport(args: string[], deps: AccountDeps): Promise<numb
|
|
|
543
538
|
clearTimeout(timer);
|
|
544
539
|
}
|
|
545
540
|
if (response.status === 0) {
|
|
546
|
-
if (!timedOut) return proxyUnreachable();
|
|
541
|
+
if (!timedOut) return proxyUnreachable(response.transportError);
|
|
547
542
|
console.error(`Error: import_timeout after ${importTimeoutMs}ms`);
|
|
548
543
|
return 1;
|
|
549
544
|
}
|
|
@@ -596,8 +591,8 @@ export async function cmdClearCooldown(args: string[], deps: AccountDeps): Promi
|
|
|
596
591
|
const baseUrl = await resolveBaseUrl(deps);
|
|
597
592
|
if (!baseUrl) return proxyUnreachable();
|
|
598
593
|
const response = await apiJson(deps, baseUrl, "POST", "/api/codex-auth/accounts/clear-cooldown", { id });
|
|
599
|
-
if (response.status === 0) return proxyUnreachable();
|
|
600
|
-
if (response.status !== 200) return apiError(response.json, `failed to clear cooldown for ${requestedId}
|
|
594
|
+
if (response.status === 0) return proxyUnreachable(response.transportError);
|
|
595
|
+
if (response.status !== 200) return apiError(response.json, `failed to clear cooldown for ${requestedId}`, response.status);
|
|
601
596
|
const cleared = response.json?.cleared === true;
|
|
602
597
|
if (wantsJson) console.log(JSON.stringify({ ok: true, provider: name, id, cleared }, null, 2));
|
|
603
598
|
else if (cleared) console.log(`${name}: cooldown lifted for ${requestedId}`);
|
|
@@ -687,8 +682,8 @@ export async function cmdPriority(args: string[], deps: AccountDeps): Promise<nu
|
|
|
687
682
|
}
|
|
688
683
|
|
|
689
684
|
const response = await apiJson(deps, baseUrl, "PUT", "/api/codex-auth/accounts/priority", { id, priority });
|
|
690
|
-
if (response.status === 0) return proxyUnreachable();
|
|
691
|
-
if (response.status !== 200) return apiError(response.json, `failed to set selection order for ${requestedId}
|
|
685
|
+
if (response.status === 0) return proxyUnreachable(response.transportError);
|
|
686
|
+
if (response.status !== 200) return apiError(response.json, `failed to set selection order for ${requestedId}`, response.status);
|
|
692
687
|
const applied = typeof response.json.priority === "number" ? response.json.priority : (priority ?? 0);
|
|
693
688
|
if (wantsJson) {
|
|
694
689
|
console.log(JSON.stringify(
|
|
@@ -710,6 +705,237 @@ export async function cmdPriority(args: string[], deps: AccountDeps): Promise<nu
|
|
|
710
705
|
return 0;
|
|
711
706
|
}
|
|
712
707
|
|
|
708
|
+
/**
|
|
709
|
+
* `ocx account pause|resume <provider> <id>` (#2702).
|
|
710
|
+
*
|
|
711
|
+
* The server routes have always existed; only the CLI caller was missing, so pausing an
|
|
712
|
+
* account was dashboard-only. The issue reports these as POST; the code is PUT
|
|
713
|
+
* (`auth-api.ts:1494`), and the route is shared by both directions with a `paused` boolean
|
|
714
|
+
* rather than being two endpoints.
|
|
715
|
+
*/
|
|
716
|
+
export async function cmdPause(args: string[], deps: AccountDeps, paused: boolean): Promise<number> {
|
|
717
|
+
const wantsJson = flag(args, "--json");
|
|
718
|
+
const name = args.shift();
|
|
719
|
+
const requestedId = args.shift();
|
|
720
|
+
const verb = paused ? "pause" : "resume";
|
|
721
|
+
if (!name || !requestedId || args.length) return usage();
|
|
722
|
+
const classified = configAndType(deps, name);
|
|
723
|
+
if ("error" in classified) return usage(`Error: ${classified.error}`);
|
|
724
|
+
if (classified.type !== "codex") {
|
|
725
|
+
return usage(`Error: ${verb} applies to the openai Codex account pool`);
|
|
726
|
+
}
|
|
727
|
+
const id = requestedId === "main" ? MAIN_ID : requestedId;
|
|
728
|
+
|
|
729
|
+
const baseUrl = await resolveBaseUrl(deps);
|
|
730
|
+
if (!baseUrl) return proxyUnreachable();
|
|
731
|
+
|
|
732
|
+
const response = await apiJson(deps, baseUrl, "PUT", "/api/codex-auth/accounts/pause", { id, paused });
|
|
733
|
+
// Transport sentinel first: status 0 means the proxy never answered, and comparing it to
|
|
734
|
+
// 200 would report an unreachable proxy as a management error.
|
|
735
|
+
if (response.status === 0) return proxyUnreachable(response.transportError);
|
|
736
|
+
if (response.status !== 200) return apiError(response.json, `failed to ${verb} ${requestedId}`, response.status);
|
|
737
|
+
|
|
738
|
+
if (wantsJson) {
|
|
739
|
+
console.log(JSON.stringify({ ok: true, provider: name, id, paused }, null, 2));
|
|
740
|
+
} else {
|
|
741
|
+
console.log(`${name}: ${requestedId} ${paused ? "paused" : "resumed"}`);
|
|
742
|
+
}
|
|
743
|
+
if (paused) {
|
|
744
|
+
// Both are server-side effects of this route (auth-api.ts:1508-1510), not consequences
|
|
745
|
+
// the operator would infer from the word "pause".
|
|
746
|
+
console.error("Threads bound to this account are unbound, and a fallback account is selected if this one was active.");
|
|
747
|
+
}
|
|
748
|
+
return 0;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* `ocx account pause-exhausted [--off]` (#2702).
|
|
753
|
+
*
|
|
754
|
+
* Pauses every account whose quota is spent. The route refreshes quota for each account, so
|
|
755
|
+
* it can partially fail; the response distinguishes "checked none and some failed" from
|
|
756
|
+
* "checked some", and that distinction is reported rather than flattened into ok/not-ok.
|
|
757
|
+
*/
|
|
758
|
+
export async function cmdPauseExhausted(args: string[], deps: AccountDeps): Promise<number> {
|
|
759
|
+
const wantsJson = flag(args, "--json");
|
|
760
|
+
const name = args.shift();
|
|
761
|
+
if (!name || args.length) return usage();
|
|
762
|
+
const classified = configAndType(deps, name);
|
|
763
|
+
if ("error" in classified) return usage(`Error: ${classified.error}`);
|
|
764
|
+
if (classified.type !== "codex") {
|
|
765
|
+
return usage("Error: pause-exhausted applies to the openai Codex account pool");
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const baseUrl = await resolveBaseUrl(deps);
|
|
769
|
+
if (!baseUrl) return proxyUnreachable();
|
|
770
|
+
|
|
771
|
+
const response = await apiJson(deps, baseUrl, "PUT", "/api/codex-auth/accounts/pause-exhausted", {});
|
|
772
|
+
if (response.status === 0) return proxyUnreachable(response.transportError);
|
|
773
|
+
if (response.status !== 200) return apiError(response.json, "failed to pause exhausted accounts", response.status);
|
|
774
|
+
|
|
775
|
+
const pausedIds = Array.isArray(response.json.pausedAccountIds)
|
|
776
|
+
? (response.json.pausedAccountIds as unknown[]).filter((value): value is string => typeof value === "string")
|
|
777
|
+
: [];
|
|
778
|
+
const checked = typeof response.json.checkedAccountCount === "number" ? response.json.checkedAccountCount : null;
|
|
779
|
+
const failed = typeof response.json.failedAccountCount === "number" ? response.json.failedAccountCount : null;
|
|
780
|
+
|
|
781
|
+
const complete = failed === null || failed === 0;
|
|
782
|
+
const ok = complete;
|
|
783
|
+
if (failed !== null && failed > 0) {
|
|
784
|
+
console.error(`Quota refresh failed for ${failed} account(s); those were not evaluated.`);
|
|
785
|
+
}
|
|
786
|
+
if (wantsJson) {
|
|
787
|
+
console.log(JSON.stringify({
|
|
788
|
+
ok,
|
|
789
|
+
complete,
|
|
790
|
+
provider: name,
|
|
791
|
+
pausedAccountIds: pausedIds,
|
|
792
|
+
checkedAccountCount: checked,
|
|
793
|
+
failedAccountCount: failed,
|
|
794
|
+
}, null, 2));
|
|
795
|
+
return ok ? 0 : 1;
|
|
796
|
+
}
|
|
797
|
+
console.log(pausedIds.length > 0
|
|
798
|
+
? `${name}: paused ${pausedIds.length} exhausted account(s): ${pausedIds.join(", ")}`
|
|
799
|
+
: `${name}: no exhausted accounts to pause`);
|
|
800
|
+
return ok ? 0 : 1;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Two pools expose strategy and sticky, and they are NOT reached the same way:
|
|
805
|
+
*
|
|
806
|
+
* | | Codex pool | Anthropic pool |
|
|
807
|
+
* |---|---|---|
|
|
808
|
+
* | read | `GET /api/codex-auth/active` | `GET /api/oauth/accounts/pool?provider=` |
|
|
809
|
+
* | write | `PUT /api/codex-auth/pool-strategy` | `PUT /api/oauth/accounts/pool` |
|
|
810
|
+
* | keys | `accountPoolStrategy`/`accountPoolStickyLimit` | `strategy`/`stickyLimit` |
|
|
811
|
+
* | body | bare field | field **plus** a mandatory `provider` |
|
|
812
|
+
*
|
|
813
|
+
* Omitting `provider` from the Anthropic write body earns a 400
|
|
814
|
+
* (`oauth-account-routes.ts:344`), so the asymmetry has to be encoded somewhere. Encoding it
|
|
815
|
+
* here keeps ONE verb pair working on both pools. The alternative the plan left open -- a second
|
|
816
|
+
* `provider-strategy`/`provider-sticky` pair -- would double the surface an operator must learn
|
|
817
|
+
* to express one idea, and a CLI that can steer one pool and not the other is exactly the trap
|
|
818
|
+
* this unit exists to remove.
|
|
819
|
+
*/
|
|
820
|
+
interface PoolTransport {
|
|
821
|
+
readPath: string;
|
|
822
|
+
writePath: string;
|
|
823
|
+
/** Response key carrying the applied strategy. */
|
|
824
|
+
strategyKey: string;
|
|
825
|
+
/** Response key carrying the applied sticky limit. */
|
|
826
|
+
stickyKey: string;
|
|
827
|
+
writeBody: (field: "strategy" | "stickyLimit", value: unknown) => Record<string, unknown>;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const CODEX_POOL_TRANSPORT: PoolTransport = {
|
|
831
|
+
readPath: "/api/codex-auth/active",
|
|
832
|
+
writePath: "/api/codex-auth/pool-strategy",
|
|
833
|
+
strategyKey: "accountPoolStrategy",
|
|
834
|
+
stickyKey: "accountPoolStickyLimit",
|
|
835
|
+
writeBody: (field, value) => ({ [field]: value }),
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
function anthropicPoolTransport(provider: string): PoolTransport {
|
|
839
|
+
return {
|
|
840
|
+
readPath: `/api/oauth/accounts/pool?provider=${encodeURIComponent(provider)}`,
|
|
841
|
+
writePath: "/api/oauth/accounts/pool",
|
|
842
|
+
strategyKey: "strategy",
|
|
843
|
+
stickyKey: "stickyLimit",
|
|
844
|
+
writeBody: (field, value) => ({ provider, [field]: value }),
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* The pool-config route supports `anthropic` only and says so with a 400. Any other OAuth
|
|
850
|
+
* provider is refused here with the same wording rather than spending a round-trip to learn it.
|
|
851
|
+
*/
|
|
852
|
+
function poolTransportFor(
|
|
853
|
+
classified: { type: "codex" | "oauth" | "api-key" },
|
|
854
|
+
name: string,
|
|
855
|
+
): PoolTransport | string {
|
|
856
|
+
if (classified.type === "codex") return CODEX_POOL_TRANSPORT;
|
|
857
|
+
if (classified.type === "oauth" && name === "anthropic") return anthropicPoolTransport(name);
|
|
858
|
+
return `pool settings apply to the openai Codex pool and the anthropic pool, not "${name}"`;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* `ocx account strategy <provider> [<name>]` and `ocx account sticky <provider> [<n>]` (#2702).
|
|
863
|
+
*
|
|
864
|
+
* One implementation rather than two near-duplicates, because strategy and sticky are two
|
|
865
|
+
* fields of one setting on every pool that has them.
|
|
866
|
+
*
|
|
867
|
+
* Values are NOT re-validated here. The server owns both contracts -- three strategy names,
|
|
868
|
+
* a 1-100 sticky bound -- and a duplicated bound is a second thing to keep in sync. Its 400
|
|
869
|
+
* is actionable now that the CLI prints `reason`.
|
|
870
|
+
*/
|
|
871
|
+
async function poolSetting(
|
|
872
|
+
args: string[],
|
|
873
|
+
deps: AccountDeps,
|
|
874
|
+
field: "strategy" | "stickyLimit",
|
|
875
|
+
): Promise<number> {
|
|
876
|
+
const wantsJson = flag(args, "--json");
|
|
877
|
+
const name = args.shift();
|
|
878
|
+
const requested = args.shift();
|
|
879
|
+
const label = field === "strategy" ? "pool strategy" : "sticky limit";
|
|
880
|
+
if (!name || args.length) return usage();
|
|
881
|
+
const classified = configAndType(deps, name);
|
|
882
|
+
if ("error" in classified) return usage(`Error: ${classified.error}`);
|
|
883
|
+
const transport = poolTransportFor(classified, name);
|
|
884
|
+
if (typeof transport === "string") return usage(`Error: ${transport}`);
|
|
885
|
+
|
|
886
|
+
const baseUrl = await resolveBaseUrl(deps);
|
|
887
|
+
if (!baseUrl) return proxyUnreachable();
|
|
888
|
+
|
|
889
|
+
// No value means show. A read must not rewrite what it is reporting.
|
|
890
|
+
if (requested === undefined) {
|
|
891
|
+
const response = await apiJson(deps, baseUrl, "GET", transport.readPath);
|
|
892
|
+
if (response.status === 0) return proxyUnreachable(response.transportError);
|
|
893
|
+
if (response.status !== 200) return apiError(response.json, `failed to read ${label}`, response.status);
|
|
894
|
+
const strategy = response.json[transport.strategyKey];
|
|
895
|
+
const sticky = response.json[transport.stickyKey];
|
|
896
|
+
if (wantsJson) {
|
|
897
|
+
// Pool-neutral key names: the two routes spell the same two settings differently, and a
|
|
898
|
+
// `--json` consumer should not have to branch on which pool answered.
|
|
899
|
+
console.log(JSON.stringify({ ok: true, provider: name, strategy, stickyLimit: sticky }, null, 2));
|
|
900
|
+
} else {
|
|
901
|
+
console.log(`${name}: ${label} is ${String(field === "strategy" ? strategy : sticky)}`);
|
|
902
|
+
}
|
|
903
|
+
return 0;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// Sent as a number when it parses as one so the server sees the type it validates;
|
|
907
|
+
// a non-numeric string still goes through and earns the server's own 400.
|
|
908
|
+
const value = field === "strategy"
|
|
909
|
+
? requested
|
|
910
|
+
: (Number.isNaN(Number(requested)) ? requested : Number(requested));
|
|
911
|
+
const response = await apiJson(deps, baseUrl, "PUT", transport.writePath, transport.writeBody(field, value));
|
|
912
|
+
if (response.status === 0) return proxyUnreachable(response.transportError);
|
|
913
|
+
if (response.status !== 200) return apiError(response.json, `failed to set ${label}`, response.status);
|
|
914
|
+
|
|
915
|
+
if (wantsJson) {
|
|
916
|
+
console.log(JSON.stringify({
|
|
917
|
+
ok: true,
|
|
918
|
+
provider: name,
|
|
919
|
+
strategy: response.json[transport.strategyKey],
|
|
920
|
+
stickyLimit: response.json[transport.stickyKey],
|
|
921
|
+
}, null, 2));
|
|
922
|
+
} else {
|
|
923
|
+
// Echo the APPLIED value from the response, not the requested one: the server normalizes,
|
|
924
|
+
// and printing the request would hide a normalization the operator should see.
|
|
925
|
+
const applied = field === "strategy" ? response.json[transport.strategyKey] : response.json[transport.stickyKey];
|
|
926
|
+
console.log(`${name}: ${label} is now ${String(applied)}`);
|
|
927
|
+
}
|
|
928
|
+
return 0;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
export function cmdStrategy(args: string[], deps: AccountDeps): Promise<number> {
|
|
932
|
+
return poolSetting(args, deps, "strategy");
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
export function cmdSticky(args: string[], deps: AccountDeps): Promise<number> {
|
|
936
|
+
return poolSetting(args, deps, "stickyLimit");
|
|
937
|
+
}
|
|
938
|
+
|
|
713
939
|
export async function cmdAlias(args: string[], deps: AccountDeps): Promise<number> {
|
|
714
940
|
const wantsJson = flag(args, "--json");
|
|
715
941
|
const name = args.shift();
|
|
@@ -735,8 +961,8 @@ export async function cmdAlias(args: string[], deps: AccountDeps): Promise<numbe
|
|
|
735
961
|
? { provider: name, accountId: id, alias }
|
|
736
962
|
: { name, id, alias };
|
|
737
963
|
const response = await apiJson(deps, baseUrl, "PUT", path, body);
|
|
738
|
-
if (response.status === 0) return proxyUnreachable();
|
|
739
|
-
if (response.status !== 200) return apiError(response.json, `failed to rename ${requestedId}
|
|
964
|
+
if (response.status === 0) return proxyUnreachable(response.transportError);
|
|
965
|
+
if (response.status !== 200) return apiError(response.json, `failed to rename ${requestedId}`, response.status);
|
|
740
966
|
const result = { ok: true, provider: name, id, alias: alias || null };
|
|
741
967
|
if (wantsJson) console.log(JSON.stringify(result, null, 2));
|
|
742
968
|
else console.log(alias ? `${name}: ${requestedId} is now “${alias}”` : `${name}: cleared alias for ${requestedId}`);
|