@bitkyc08/opencodex 2.77.0 → 2.78.0
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/bin/ocx.mjs +11 -10
- package/gui/dist/assets/App-BtfG5id-.js +54 -0
- package/gui/dist/assets/App-lCIslZ1L.css +1 -0
- package/gui/dist/assets/{Tray-gloIYNaW.js → Tray-DiJpSub0.js} +1 -1
- package/gui/dist/assets/index-Btc3Ogw4.js +94 -0
- package/gui/dist/assets/index-DmQB8D_R.css +1 -0
- package/gui/dist/assets/quota-summary-B_a96nXj.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +19 -2
- package/src/adapters/anthropic/client-identity.ts +13 -4
- package/src/adapters/anthropic/native-client-preamble.ts +31 -0
- package/src/adapters/anthropic/passthrough.ts +103 -25
- package/src/adapters/devin/cloud-direct/chat.ts +7 -2
- package/src/adapters/devin/trajectory.ts +49 -0
- package/src/adapters/devin.ts +13 -0
- package/src/adapters/ollama-native.ts +35 -19
- package/src/claude/intercept/client-hello.ts +103 -0
- package/src/claude/intercept/model-bindings.ts +3 -2
- package/src/claude/intercept/picker-listener.ts +279 -66
- package/src/cli/access-audio-input.ts +84 -0
- package/src/cli/access-audio-live.ts +99 -0
- package/src/cli/access-audio.ts +123 -0
- package/src/cli/access-data-client.ts +127 -0
- package/src/cli/access-data-plane.ts +104 -0
- package/src/cli/access-data-response.ts +76 -0
- package/src/cli/access.ts +65 -2
- package/src/cli/account-api.ts +30 -6
- package/src/cli/account-auth.ts +111 -42
- package/src/cli/account-key-quota.ts +118 -0
- package/src/cli/account-main.ts +11 -4
- package/src/cli/account-next-actions.ts +63 -0
- package/src/cli/account-policy-dto.ts +68 -0
- package/src/cli/account-policy.ts +167 -0
- package/src/cli/account-target.ts +8 -0
- package/src/cli/account.ts +34 -8
- package/src/cli/agent-runtime-settings.ts +130 -0
- package/src/cli/agent-settings.ts +101 -0
- package/src/cli/agent.ts +19 -2
- package/src/cli/alias.ts +7 -2
- package/src/cli/aside-profiles.ts +2 -2
- package/src/cli/capabilities-access-remote.ts +147 -0
- package/src/cli/capabilities-accounts.ts +195 -0
- package/src/cli/capabilities-agents-routing.ts +414 -0
- package/src/cli/capabilities-base.ts +783 -0
- package/src/cli/capabilities-command.ts +1 -0
- package/src/cli/capabilities-integrations.ts +264 -0
- package/src/cli/capabilities-lab.ts +214 -0
- package/src/cli/capabilities-observe-system.ts +662 -0
- package/src/cli/capabilities-provider-models.ts +302 -0
- package/src/cli/capabilities.ts +30 -1199
- package/src/cli/capability-types.ts +54 -0
- package/src/cli/catalog-command-result.ts +57 -0
- package/src/cli/chatgpt-command.ts +25 -2
- package/src/cli/claude-agent-startup-sync.ts +17 -0
- package/src/cli/claude-desktop-profile.ts +100 -0
- package/src/cli/claude-desktop.ts +39 -6
- package/src/cli/codex-shim-autorestore.ts +2 -1
- package/src/cli/codex-shim-readiness.ts +23 -3
- package/src/cli/combo-input.ts +88 -0
- package/src/cli/combo-result.ts +52 -0
- package/src/cli/combo-stats.ts +123 -0
- package/src/cli/combo.ts +37 -14
- package/src/cli/companion-timeline.ts +142 -0
- package/src/cli/companion-usage.ts +146 -0
- package/src/cli/companion.ts +12 -3
- package/src/cli/config-command.ts +24 -8
- package/src/cli/dispatch.ts +52 -79
- package/src/cli/doctor.ts +22 -4
- package/src/cli/help-catalog.ts +31 -14
- package/src/cli/help-recovery.ts +2 -2
- package/src/cli/help.ts +25 -11
- package/src/cli/index.ts +38 -27
- package/src/cli/injection-follow.ts +35 -0
- package/src/cli/inspect.ts +5 -0
- package/src/cli/integration-aside-sync.ts +69 -0
- package/src/cli/integration-cursor.ts +106 -0
- package/src/cli/integration-input.ts +60 -0
- package/src/cli/integration-journal.ts +49 -0
- package/src/cli/integration-plan-dto.ts +156 -0
- package/src/cli/integration-preview.ts +215 -0
- package/src/cli/integrations.ts +24 -18
- package/src/cli/json-input.ts +134 -0
- package/src/cli/link.ts +32 -4
- package/src/cli/local-sync-result.ts +41 -0
- package/src/cli/log-follow.ts +84 -0
- package/src/cli/log-view-filter.ts +146 -0
- package/src/cli/logout-command.ts +44 -0
- package/src/cli/model-picker-ordering.ts +137 -0
- package/src/cli/models-custom-input.ts +107 -0
- package/src/cli/models-custom-runtime.ts +112 -0
- package/src/cli/models-order.ts +194 -0
- package/src/cli/models-runtime-subcommands.ts +2 -0
- package/src/cli/models-runtime.ts +7 -0
- package/src/cli/models.ts +66 -81
- package/src/cli/observe-stream.ts +142 -0
- package/src/cli/observe.ts +75 -34
- package/src/cli/provider-batch.ts +77 -0
- package/src/cli/provider-lifecycle-runtime.ts +170 -0
- package/src/cli/provider-result.ts +124 -0
- package/src/cli/provider-runtime.ts +15 -12
- package/src/cli/provider-settings.ts +143 -0
- package/src/cli/provider.ts +112 -34
- package/src/cli/registry.ts +62 -20
- package/src/cli/remote-workspace-hub.ts +83 -0
- package/src/cli/remote-workspace.ts +6 -1
- package/src/cli/restart-failure.ts +27 -0
- package/src/cli/root.ts +12 -0
- package/src/cli/route-policy-write.ts +137 -0
- package/src/cli/route-policy.ts +8 -0
- package/src/cli/runtime-api.ts +56 -79
- package/src/cli/secret-args.ts +149 -0
- package/src/cli/settings-result.ts +30 -0
- package/src/cli/storage.ts +77 -7
- package/src/cli/system-command.ts +60 -2
- package/src/cli/system-restart-client.ts +20 -3
- package/src/cli/system-settings-parity.ts +83 -0
- package/src/cli/update-restart-candidate.ts +21 -0
- package/src/cli/update-restart-child.ts +81 -0
- package/src/cli/update-restart-home.ts +38 -0
- package/src/cli/update-restart-transport.ts +193 -0
- package/src/cli/update-restart.ts +260 -0
- package/src/cli/usage-model-search.ts +58 -0
- package/src/cli/usage-report.ts +13 -5
- package/src/cli/v2-input.ts +66 -0
- package/src/cli/v2-local-output.ts +37 -0
- package/src/cli/v2-runtime.ts +90 -0
- package/src/cli/v2.ts +102 -144
- package/src/clients/config-export/dsh.ts +21 -0
- package/src/clients/config-export.ts +7 -1
- package/src/codex/catalog/build-entries.ts +1 -1
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/heal-observation.ts +66 -0
- package/src/codex/catalog/publication-observer.ts +19 -0
- package/src/codex/catalog/remote.ts +13 -15
- package/src/codex/catalog/restore.ts +19 -6
- package/src/codex/catalog/retained-sync.ts +58 -10
- package/src/codex/catalog/routed-removal.ts +116 -0
- package/src/codex/catalog/write-audit-contract.ts +21 -0
- package/src/codex/catalog/write-audit.ts +171 -0
- package/src/codex/catalog-self-heal.ts +284 -0
- package/src/codex/catalog-write-serialization.ts +71 -7
- package/src/codex/codex-home-owner.ts +124 -0
- package/src/codex/convergence.ts +55 -15
- package/src/codex/features.ts +8 -1
- package/src/codex/inject/restore.ts +46 -4
- package/src/codex/inject.ts +11 -1
- package/src/codex/internal/catalog-writer.ts +120 -13
- package/src/codex/journal.ts +49 -13
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/quota-types.ts +3 -0
- package/src/codex/quota.ts +13 -3
- package/src/codex/refresh.ts +20 -9
- package/src/codex/runtime.ts +7 -1
- package/src/codex/shim-diagnostics.ts +80 -0
- package/src/codex/shim-fingerprint.ts +7 -2
- package/src/codex/shim-migration.ts +153 -0
- package/src/codex/shim-overlay.ts +509 -0
- package/src/codex/shim-path-resolution.ts +14 -2
- package/src/codex/shim-probe.ts +4 -3
- package/src/codex/shim-state-file.ts +93 -7
- package/src/codex/shim.ts +15 -68
- package/src/codex/sync.ts +24 -4
- package/src/combos/failover.ts +19 -0
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +12 -0
- package/src/config/atomic-write.ts +13 -8
- package/src/config/diagnostics.ts +19 -1
- package/src/config/load-degrade.ts +3 -0
- package/src/config/schema/chatgpt-desktop.ts +22 -0
- package/src/config/schema/config-schema.ts +32 -14
- package/src/config/schema/leaf-validators.ts +3 -2
- package/src/generated/compatibility-version.json +445 -129
- package/src/integrations/merge.ts +13 -5
- package/src/integrations/mutation-plan.ts +46 -3
- package/src/integrations/omp-yaml-source.ts +181 -1
- package/src/integrations/ownership-policy.ts +66 -0
- package/src/integrations/registry.ts +50 -1
- package/src/integrations/state.ts +46 -21
- package/src/integrations/target.ts +25 -2
- package/src/integrations/writer.ts +89 -31
- package/src/lib/package-tree-integrity.ts +42 -1
- package/src/lib/proxy-env.ts +3 -0
- package/src/lib/test-home-guard.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +19 -8
- package/src/oauth/anthropic-routing.ts +17 -12
- package/src/oauth/anthropic.ts +27 -2
- package/src/oauth/index.ts +6 -8
- package/src/oauth/pool-settings-capability.ts +9 -2
- package/src/protocols/plan-snapshot.ts +1 -1
- package/src/protocols/settings.ts +28 -4
- package/src/server/caller-session-identity.ts +32 -0
- package/src/server/claude-messages.ts +11 -4
- package/src/server/index/serve-options.ts +12 -7
- package/src/server/index.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +6 -5
- package/src/server/management/context.ts +8 -1
- package/src/server/management/cursor-integration-routes.ts +3 -3
- package/src/server/management/integration-routes.ts +5 -5
- package/src/server/management/model-routes.ts +3 -3
- package/src/server/management/native-integration-routes.ts +2 -2
- package/src/server/management/oauth-account-routes.ts +96 -30
- package/src/server/management/route-registry.ts +25 -24
- package/src/server/messages-native-eligibility.ts +3 -6
- package/src/server/messages-native-oauth.ts +82 -35
- package/src/server/messages-native.ts +78 -16
- package/src/server/responses/adapter-dispatch.ts +53 -28
- package/src/server/responses/core-auth.ts +10 -4
- package/src/server/responses/core-combo.ts +46 -6
- package/src/service/cli.ts +6 -1
- package/src/service/ownership-mutation-lease.d.mts +26 -0
- package/src/service/ownership-mutation-lease.mjs +76 -1
- package/src/types/config.ts +2 -0
- package/src/types/provider.ts +10 -10
- package/src/update/index.ts +2 -1
- package/src/update/update-failure-guidance.d.mts +6 -0
- package/src/update/update-failure-guidance.mjs +30 -5
- package/gui/dist/assets/App-COdEeqSj.css +0 -1
- package/gui/dist/assets/App-DEpCJEP_.js +0 -54
- package/gui/dist/assets/index-Cd7ittMv.css +0 -1
- package/gui/dist/assets/index-DwJKFJm5.js +0 -94
- package/gui/dist/assets/quota-summary-DGcEJU3h.js +0 -1
package/src/cli/access.ts
CHANGED
|
@@ -4,15 +4,20 @@ import {
|
|
|
4
4
|
rejectArgs,
|
|
5
5
|
runCliAction,
|
|
6
6
|
runtimeRequest,
|
|
7
|
+
runtimeBaseUrl,
|
|
7
8
|
takeFlag,
|
|
8
9
|
takeOption,
|
|
9
10
|
type RuntimeApiDeps,
|
|
10
11
|
} from "./runtime-api";
|
|
11
12
|
|
|
13
|
+
import { handleSelectedKeyTest, type SelectedKeyTestDeps } from "./access-data-plane";
|
|
14
|
+
import { handleAccessAudioCommand } from "./access-audio";
|
|
15
|
+
|
|
12
16
|
const USAGE = `Usage:
|
|
13
17
|
ocx access key [list] [--json]
|
|
14
18
|
ocx access key create [name] [--json]
|
|
15
19
|
ocx access key get <id-or-name> [--json]
|
|
20
|
+
ocx access key rename <id-or-name> <name> [--json]
|
|
16
21
|
ocx access key set <id-or-name> [--allow-provider <name>]... [--allow-model <id>]... [--clear] [--json]
|
|
17
22
|
ocx access key rotate <id> [--json]
|
|
18
23
|
ocx access key rotate commit <id> <rotation-id> [--json]
|
|
@@ -20,7 +25,9 @@ const USAGE = `Usage:
|
|
|
20
25
|
ocx access key remove <id> --yes [--json]
|
|
21
26
|
ocx access endpoints [--json]
|
|
22
27
|
ocx access models [--json]
|
|
23
|
-
ocx access test <model> [--protocol <chat|responses|messages>] [--json]
|
|
28
|
+
ocx access test <model> [--protocol <chat|responses|messages>] [--api-key-stdin] [--json]
|
|
29
|
+
ocx access audio transcribe <file> --model <id> --api-key-stdin [--json]
|
|
30
|
+
ocx access audio live-check --model <id> --api-key-stdin [--json]`;
|
|
24
31
|
|
|
25
32
|
const UTC_ISO_INSTANT_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
|
|
26
33
|
|
|
@@ -234,6 +241,56 @@ async function key(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
234
241
|
throw new CliUsageError(`unknown key command ${action}`, USAGE);
|
|
235
242
|
}
|
|
236
243
|
|
|
244
|
+
/** Rename never sends or prints a secret, and cannot silently choose duplicate ids. */
|
|
245
|
+
async function renameKey(argv: string[], deps: RuntimeApiDeps): Promise<number> {
|
|
246
|
+
const args = [...argv];
|
|
247
|
+
const wantsJson = takeFlag(args, "--json");
|
|
248
|
+
const [selector, rawName] = args;
|
|
249
|
+
if (args.length !== 2 || !selector?.trim() || selector.startsWith("-") || !rawName
|
|
250
|
+
|| /[\u0000-\u001f\u007f]/.test(rawName) || !rawName.trim() || rawName.trim().length > 64) {
|
|
251
|
+
console.error("Error: Supply one key id or unambiguous name and a new name of 1–64 characters without controls.");
|
|
252
|
+
return 2;
|
|
253
|
+
}
|
|
254
|
+
const name = rawName.trim();
|
|
255
|
+
try {
|
|
256
|
+
const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
|
|
257
|
+
const before = await runtimeRequest<unknown>("/api/keys", { redirect: "error", credentials: "omit" }, pinned);
|
|
258
|
+
if (!before || typeof before !== "object" || !("keys" in before) || !Array.isArray(before.keys)
|
|
259
|
+
|| before.keys.some(row => !row || typeof row !== "object" || Array.isArray(row)
|
|
260
|
+
|| typeof row.id !== "string" || !row.id || typeof row.name !== "string")) throw new Error();
|
|
261
|
+
const rows = before.keys as Array<Record<string, unknown>>;
|
|
262
|
+
const wanted = selector.trim().toLowerCase();
|
|
263
|
+
if (rows.filter(row => (row.id as string).toLowerCase() === wanted).length > 1) {
|
|
264
|
+
throw new CliUsageError("Ambiguous key identity.");
|
|
265
|
+
}
|
|
266
|
+
const target = findKeyRow(rows, selector);
|
|
267
|
+
if (rows.filter(row => (row.id as string).toLowerCase() === (target.id as string).toLowerCase()).length !== 1) {
|
|
268
|
+
throw new CliUsageError("Ambiguous key identity.");
|
|
269
|
+
}
|
|
270
|
+
const result = await runtimeRequest<Record<string, unknown>>("/api/keys", {
|
|
271
|
+
method: "PATCH", body: JSON.stringify({ id: target.id, name }), redirect: "error", credentials: "omit",
|
|
272
|
+
}, pinned);
|
|
273
|
+
if (!result || typeof result !== "object" || Array.isArray(result) || result.id !== target.id || result.name !== name
|
|
274
|
+
|| typeof result.createdAt !== "string" || !isUtcIsoInstant(result.createdAt)) throw new Error();
|
|
275
|
+
const projected: Record<string, unknown> = { id: result.id, name: result.name, createdAt: result.createdAt };
|
|
276
|
+
for (const scope of ["allowedProviders", "allowedModels"] as const) {
|
|
277
|
+
if (result[scope] === undefined) continue;
|
|
278
|
+
const value = result[scope];
|
|
279
|
+
if (!Array.isArray(value) || value.some(item => typeof item !== "string" || !item.trim() || item.length > 256)) throw new Error();
|
|
280
|
+
projected[scope] = [...value];
|
|
281
|
+
}
|
|
282
|
+
printData(projected, wantsJson, ["API key renamed.", ...scopeLines(projected)]);
|
|
283
|
+
return 0;
|
|
284
|
+
} catch (error) {
|
|
285
|
+
if (error instanceof CliUsageError) {
|
|
286
|
+
console.error("Error: API key selector is missing or ambiguous. List keys and use a unique id.");
|
|
287
|
+
return 2;
|
|
288
|
+
}
|
|
289
|
+
console.error("Error: API key rename was not confirmed. List keys to check the current name before retrying.");
|
|
290
|
+
return 1;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
237
294
|
async function endpoints(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
238
295
|
const args = [...argv];
|
|
239
296
|
const wantsJson = takeFlag(args, "--json");
|
|
@@ -271,7 +328,13 @@ async function testModel(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
271
328
|
printData(result, wantsJson, [`${model}: ${protocol} request succeeded.`]);
|
|
272
329
|
}
|
|
273
330
|
|
|
274
|
-
export async function handleAccessCommand(argv: string[], deps:
|
|
331
|
+
export async function handleAccessCommand(argv: string[], deps: SelectedKeyTestDeps = {}): Promise<number> {
|
|
332
|
+
const [command, ...args] = argv;
|
|
333
|
+
if (command === "audio") return handleAccessAudioCommand(args, deps);
|
|
334
|
+
if (command === "test" && args.some(arg => arg === "--api-key-stdin" || arg.startsWith("--api-key-stdin="))) {
|
|
335
|
+
return handleSelectedKeyTest(args, deps);
|
|
336
|
+
}
|
|
337
|
+
if ((command === "key" || command === "keys") && args[0] === "rename") return renameKey(args.slice(1), deps);
|
|
275
338
|
return runCliAction(async () => {
|
|
276
339
|
const [sub = "key", ...rest] = argv;
|
|
277
340
|
if (sub === "key" || sub === "keys") await key(rest, deps);
|
package/src/cli/account-api.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseQuotaFailureCode, type QuotaFailureCode } from "../providers/quota-types";
|
|
1
|
+
import { parseQuotaFailureCode, type QuotaFailureCode, type ProviderQuota, type AccountQuotaMode } from "../providers/quota-types";
|
|
2
2
|
/**
|
|
3
3
|
* Data-access layer for `ocx account` (issue #180) — live-proxy HTTP client and
|
|
4
4
|
* per-family account readers. Kept separate from account.ts (command handlers)
|
|
@@ -11,6 +11,10 @@ import { getProviderRegistryEntry, providerCodexAccountMode } from "../providers
|
|
|
11
11
|
import type { OcxConfig } from "../types";
|
|
12
12
|
import { projectCodexQuotaRefreshOutcome, type CodexQuotaRefreshOutcome } from "../codex/quota-refresh-outcome";
|
|
13
13
|
|
|
14
|
+
import { projectApiKeyQuotaRows } from "./account-key-quota";
|
|
15
|
+
import { projectAccountHealth } from "./account-next-actions";
|
|
16
|
+
import type { OAuthHealthLabel } from "../oauth/health";
|
|
17
|
+
|
|
14
18
|
export type AccountType = "codex" | "oauth" | "api-key";
|
|
15
19
|
|
|
16
20
|
export interface AccountRow {
|
|
@@ -27,6 +31,11 @@ export interface AccountRow {
|
|
|
27
31
|
masked?: string;
|
|
28
32
|
active: boolean;
|
|
29
33
|
needsReauth?: boolean;
|
|
34
|
+
/** Validated server health label and locally generated recovery guidance. */
|
|
35
|
+
health?: OAuthHealthLabel;
|
|
36
|
+
healthAction?: string;
|
|
37
|
+
/** Explicit paid-credit consent, reported only when supplied by the Codex API. */
|
|
38
|
+
creditsAfterLimit?: boolean;
|
|
30
39
|
needsReauthReason?: "verify_account";
|
|
31
40
|
autoSelectable?: boolean;
|
|
32
41
|
skipReason?: "paused" | "needs_reauth" | "suspended" | "cooldown" | "quota_exhausted";
|
|
@@ -38,7 +47,8 @@ export interface AccountRow {
|
|
|
38
47
|
priority?: number;
|
|
39
48
|
/** Null means the account inherits the global usage-switch threshold. */
|
|
40
49
|
autoSwitchThresholdOverride?: number | null;
|
|
41
|
-
quota?: CodexQuotaDto | null;
|
|
50
|
+
quota?: (CodexQuotaDto & Partial<ProviderQuota>) | null;
|
|
51
|
+
quotaMode?: AccountQuotaMode;
|
|
42
52
|
quotaRefresh?: CodexQuotaRefreshOutcome;
|
|
43
53
|
quotaUnavailable?: boolean;
|
|
44
54
|
quotaFailure?: QuotaFailureCode;
|
|
@@ -123,7 +133,7 @@ export async function apiJson(
|
|
|
123
133
|
method: "GET" | "PUT" | "POST" | "DELETE",
|
|
124
134
|
path: string,
|
|
125
135
|
body?: unknown,
|
|
126
|
-
options: { signal?: AbortSignal } = {},
|
|
136
|
+
options: { signal?: AbortSignal; redirect?: RequestRedirect } = {},
|
|
127
137
|
): Promise<ApiResult> {
|
|
128
138
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
129
139
|
try {
|
|
@@ -132,6 +142,7 @@ export async function apiJson(
|
|
|
132
142
|
headers: runningProxyUpdateHeaders(),
|
|
133
143
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
134
144
|
signal: options.signal,
|
|
145
|
+
...(options.redirect ? { redirect: options.redirect } : {}),
|
|
135
146
|
});
|
|
136
147
|
const json = (await res.json().catch(() => ({}))) as Record<string, unknown>;
|
|
137
148
|
return { status: res.status, json };
|
|
@@ -256,6 +267,8 @@ interface CodexAccountDto {
|
|
|
256
267
|
selectionExcludedReason?: "plan_excluded";
|
|
257
268
|
selectionExcludedPlan?: string;
|
|
258
269
|
health?: { reason?: string };
|
|
270
|
+
healthLabel?: unknown;
|
|
271
|
+
creditsAfterLimit?: unknown;
|
|
259
272
|
priority?: number;
|
|
260
273
|
autoSwitchThresholdOverride?: number | null;
|
|
261
274
|
quota?: CodexQuotaDto | null;
|
|
@@ -322,6 +335,8 @@ export async function fetchCodexRows(
|
|
|
322
335
|
plan: a.plan,
|
|
323
336
|
active: a.id === activeId,
|
|
324
337
|
needsReauth: a.needsReauth,
|
|
338
|
+
...projectAccountHealth(a, "openai", a.id),
|
|
339
|
+
...(typeof a.creditsAfterLimit === "boolean" ? { creditsAfterLimit: a.creditsAfterLimit } : {}),
|
|
325
340
|
...(a.selectionExcludedReason === "plan_excluded" ? {
|
|
326
341
|
selectionExcludedReason: "plan_excluded" as const,
|
|
327
342
|
...(typeof a.selectionExcludedPlan === "string" ? { selectionExcludedPlan: a.selectionExcludedPlan } : {}),
|
|
@@ -346,6 +361,7 @@ interface OAuthAccountDto {
|
|
|
346
361
|
email?: string;
|
|
347
362
|
active?: boolean;
|
|
348
363
|
needsReauth?: boolean;
|
|
364
|
+
healthLabel?: unknown;
|
|
349
365
|
/** Present only for providers that support operator pause (generic OAuth pools). */
|
|
350
366
|
paused?: boolean;
|
|
351
367
|
autoSwitchThresholdOverride?: number | null;
|
|
@@ -390,6 +406,7 @@ async function fetchOAuthRows(
|
|
|
390
406
|
email: a.email,
|
|
391
407
|
active: a.active ?? a.id === activeId,
|
|
392
408
|
needsReauth: a.needsReauth,
|
|
409
|
+
...projectAccountHealth(a, name, a.id),
|
|
393
410
|
...(a.paused === true ? { paused: true } : {}),
|
|
394
411
|
...(name === "anthropic" && Object.hasOwn(a, "autoSwitchThresholdOverride") ? { autoSwitchThresholdOverride: a.autoSwitchThresholdOverride } : {}),
|
|
395
412
|
...(a.needsReauthReason === "verify_account" ? { needsReauthReason: a.needsReauthReason } : {}),
|
|
@@ -416,12 +433,19 @@ interface ApiKeyDto {
|
|
|
416
433
|
active?: boolean;
|
|
417
434
|
}
|
|
418
435
|
|
|
419
|
-
async function fetchKeyRows(deps: AccountDeps, baseUrl: string, name: string): Promise<FamilyRows> {
|
|
420
|
-
const
|
|
436
|
+
async function fetchKeyRows(deps: AccountDeps, baseUrl: string, name: string, quota?: { refresh?: boolean }): Promise<FamilyRows> {
|
|
437
|
+
const query = `?name=${encodeURIComponent(name)}${quota ? `"a=1${quota.refresh ? "&refresh=1" : ""}` : ""}`;
|
|
438
|
+
const res = await apiJson(deps, baseUrl, "GET", `/api/providers/keys${query}`, undefined, quota ? { redirect: "error" } : {});
|
|
421
439
|
if (res.status === 0) {
|
|
422
440
|
return { rows: [], activeId: null, status: 0, networkDown: true, transportError: res.transportError };
|
|
423
441
|
}
|
|
424
442
|
if (res.status !== 200) return { rows: [], activeId: null, status: res.status, errorJson: res.json };
|
|
443
|
+
if (quota) {
|
|
444
|
+
try { return projectApiKeyQuotaRows(res.json, name); }
|
|
445
|
+
catch (error) {
|
|
446
|
+
return { rows: [], activeId: null, status: 200, errorJson: { error: error instanceof Error ? error.message : "Malformed API-key quota response." } };
|
|
447
|
+
}
|
|
448
|
+
}
|
|
425
449
|
const activeId = typeof res.json.activeId === "string" ? res.json.activeId : null;
|
|
426
450
|
const keys = Array.isArray(res.json.keys) ? res.json.keys as ApiKeyDto[] : [];
|
|
427
451
|
const rows = keys.map(k => ({
|
|
@@ -444,7 +468,7 @@ export function fetchRows(
|
|
|
444
468
|
): Promise<FamilyRows> {
|
|
445
469
|
if (type === "codex") return fetchCodexRows(deps, baseUrl, Boolean(quota?.refresh), quota !== undefined);
|
|
446
470
|
if (type === "oauth") return fetchOAuthRows(deps, baseUrl, name, quota);
|
|
447
|
-
return fetchKeyRows(deps, baseUrl, name);
|
|
471
|
+
return fetchKeyRows(deps, baseUrl, name, quota);
|
|
448
472
|
}
|
|
449
473
|
|
|
450
474
|
export async function fetchProviderQuotaReport(
|
package/src/cli/account-auth.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { writeSync } from "node:fs";
|
|
2
|
+
import { isValidProviderName } from "../config/provider-name";
|
|
3
|
+
import { runCatalogAction } from "./catalog-command-result";
|
|
2
4
|
import { modelSelectionGuidance, modelSelectionNextSteps } from "./model-selection-guidance";
|
|
3
5
|
import { warnIfCodexCatalogRefreshPending } from "./account-catalog-refresh";
|
|
4
6
|
import { isCodexResetCreditOperationId } from "../codex/reset-credit-recovery";
|
|
@@ -10,6 +12,8 @@ import {
|
|
|
10
12
|
rejectArgs,
|
|
11
13
|
runCliAction,
|
|
12
14
|
runtimeRequest,
|
|
15
|
+
runtimeBaseUrl,
|
|
16
|
+
terminalSafeText,
|
|
13
17
|
takeFlag,
|
|
14
18
|
takeOption,
|
|
15
19
|
takeOptionWithSyntax,
|
|
@@ -34,7 +38,7 @@ function writeStdoutFully(text: string): void {
|
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
const USAGE = `Usage:
|
|
37
|
-
ocx account login <provider> [--id <account-id>] [--reauth] [--device] [--method builder-id|google|github] [--code -] [--no-wait] [--json]
|
|
41
|
+
ocx account login <provider> [--id <account-id>] [--reauth] [--device] [--method builder-id|google|github] [--open-browser on|off] [--add-account on|off] [--code -] [--no-wait] [--json]
|
|
38
42
|
ocx account code <provider> [--flow <flow-id>] [--json] (reads the code from stdin)
|
|
39
43
|
ocx account cancel <provider> [--flow <flow-id>] [--json] (--flow required for codex)
|
|
40
44
|
ocx account reset-credits <account-id|main> [--consume --yes [--operation-id <uuid>]] [--json]
|
|
@@ -81,6 +85,62 @@ interface LoginStart {
|
|
|
81
85
|
warning?: string;
|
|
82
86
|
}
|
|
83
87
|
|
|
88
|
+
/** Public handoff only. Never spread runtime bodies into terminal or JSON output. */
|
|
89
|
+
function loginObject(value: unknown): Record<string, unknown> {
|
|
90
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid login reply");
|
|
91
|
+
return value as Record<string, unknown>;
|
|
92
|
+
}
|
|
93
|
+
function publicLoginStart(value: unknown, kiro = false): LoginStart {
|
|
94
|
+
const data = loginObject(value);
|
|
95
|
+
const out: LoginStart = {};
|
|
96
|
+
const strings = kiro
|
|
97
|
+
? ["flowId", "userCode", "verificationUri", "verificationUriComplete"] as const
|
|
98
|
+
: ["url", "flowId", "instructions", "deviceCode"] as const;
|
|
99
|
+
for (const key of strings) {
|
|
100
|
+
if (data[key] !== undefined && typeof data[key] !== "string") throw new Error("Invalid login handoff field");
|
|
101
|
+
if (typeof data[key] === "string") out[key] = data[key];
|
|
102
|
+
}
|
|
103
|
+
if (typeof data.browserLaunch === "string" && ["started", "failed", "skipped"].includes(data.browserLaunch)) out.browserLaunch = data.browserLaunch as LoginStart["browserLaunch"];
|
|
104
|
+
if (typeof data.method === "string" && ["builder-id", "google", "github"].includes(data.method)) out.method = data.method as LoginStart["method"];
|
|
105
|
+
if (typeof data.state === "string" && ["pending", "done", "failed", "expired", "cancelled"].includes(data.state)) out.state = data.state as string;
|
|
106
|
+
if (typeof data.warning === "string" && ["duplicate_profile_arn", "manual_review_required"].includes(data.warning)) out.warning = data.warning as string;
|
|
107
|
+
if (typeof data.expiresAt === "number" && Number.isFinite(data.expiresAt)) out.expiresAt = data.expiresAt;
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
function publicLoginStatus(value: unknown): Record<string, unknown> {
|
|
111
|
+
const data = loginObject(value);
|
|
112
|
+
const out: Record<string, unknown> = {};
|
|
113
|
+
for (const key of ["accountId", "activeAccountId", "email"]) if (typeof data[key] === "string") out[key] = data[key];
|
|
114
|
+
for (const key of ["loggedIn", "done", "needsReauth", "validationPending", "catalogRefreshPending"]) {
|
|
115
|
+
if (data[key] !== undefined && typeof data[key] !== "boolean") throw new Error("Invalid login completion field");
|
|
116
|
+
if (typeof data[key] === "boolean") out[key] = data[key];
|
|
117
|
+
}
|
|
118
|
+
if (typeof data.status === "string" && ["starting", "pending", "done", "error", "expired", "cancelled", "idle"].includes(data.status)) out.status = data.status;
|
|
119
|
+
if (typeof data.source === "string" && ["oauth", "local-cli", "credential-file", "environment", "manual"].includes(data.source)) out.source = data.source;
|
|
120
|
+
if (data.hint && typeof data.hint === "object" && !Array.isArray(data.hint)) out.hint = publicLoginStart(data.hint);
|
|
121
|
+
if (Array.isArray(data.accounts)) out.accounts = data.accounts.map(value => {
|
|
122
|
+
const row = loginObject(value);
|
|
123
|
+
const account: Record<string, unknown> = {};
|
|
124
|
+
for (const key of ["id", "alias", "email"]) if (typeof row[key] === "string") account[key] = row[key];
|
|
125
|
+
for (const key of ["active", "needsReauth"]) if (typeof row[key] === "boolean") account[key] = row[key];
|
|
126
|
+
if (row.needsReauthReason === "verify_account") account.needsReauthReason = "verify_account";
|
|
127
|
+
if (typeof row.expiresAt === "number" && Number.isFinite(row.expiresAt)) account.expiresAt = row.expiresAt;
|
|
128
|
+
if (row.plan === null) account.plan = null;
|
|
129
|
+
return account;
|
|
130
|
+
});
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
function loginBoolean(args: string[], flag: string): boolean | undefined {
|
|
134
|
+
const option = takeOptionWithSyntax(args, flag);
|
|
135
|
+
if (!option) return undefined;
|
|
136
|
+
if (option.value !== "on" && option.value !== "off") throw new CliUsageError(`${flag} must be on or off`, USAGE);
|
|
137
|
+
return option.value === "on";
|
|
138
|
+
}
|
|
139
|
+
function writeLoginInstructions(lines: readonly string[]): void {
|
|
140
|
+
const block = lines.filter(line => line !== "").flatMap(line => line.split("\n")).map(terminalSafeText).join("\n");
|
|
141
|
+
if (block) writeStdoutFully(`${block}\n`);
|
|
142
|
+
}
|
|
143
|
+
|
|
84
144
|
/**
|
|
85
145
|
* Said only when the host could not open a browser (#5261).
|
|
86
146
|
*
|
|
@@ -136,7 +196,7 @@ async function resolveCode(
|
|
|
136
196
|
return await readSecretLine(deps, "authorization code");
|
|
137
197
|
}
|
|
138
198
|
|
|
139
|
-
async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
199
|
+
async function login(argv: string[], deps: RuntimeApiDeps): Promise<number | void> {
|
|
140
200
|
const args = [...argv];
|
|
141
201
|
const provider = args.shift()?.trim().toLowerCase();
|
|
142
202
|
const wantsJson = takeFlag(args, "--json");
|
|
@@ -146,34 +206,39 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
146
206
|
const method = takeOption(args, "--method");
|
|
147
207
|
const id = takeOption(args, "--id");
|
|
148
208
|
const suppliedCode = takeOptionWithSyntax(args, "--code");
|
|
209
|
+
const openBrowser = loginBoolean(args, "--open-browser");
|
|
210
|
+
const addAccount = loginBoolean(args, "--add-account");
|
|
211
|
+
if (provider && !isValidProviderName(provider)) throw new CliUsageError("invalid provider name", USAGE);
|
|
149
212
|
if (!provider) throw new CliUsageError("provider is required", USAGE);
|
|
150
213
|
if (method !== undefined) {
|
|
151
214
|
if (provider !== "kiro" || !["builder-id", "google", "github"].includes(method)) {
|
|
152
215
|
throw new CliUsageError("--method requires kiro and builder-id, google, or github", USAGE);
|
|
153
216
|
}
|
|
154
217
|
if (reauth || id) throw new CliUsageError("native Kiro device login only adds accounts; remove and re-add to reauthenticate", USAGE);
|
|
218
|
+
if (openBrowser !== undefined || addAccount !== undefined) throw new CliUsageError("native Kiro --method does not accept --open-browser or --add-account", USAGE);
|
|
155
219
|
if (device || suppliedCode) throw new CliUsageError("--method cannot be combined with --device or --code", USAGE);
|
|
156
220
|
}
|
|
157
|
-
// A
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
// one word away from a command people run constantly. Flag-shaped leftovers stay visible,
|
|
161
|
-
// because a mistyped flag is exactly what the message has to name.
|
|
162
|
-
rejectArgs(args, USAGE, { redactValues: true });
|
|
221
|
+
// A malformed leftover may contain pasted authorization input, even inside an
|
|
222
|
+
// unknown flag. New login parsing reports only a fixed usage diagnostic.
|
|
223
|
+
if (args.length) throw new CliUsageError("Unexpected arguments or repeated options", USAGE);
|
|
163
224
|
// kimi, nous, and github-copilot are already device flows, so --device is a
|
|
164
225
|
// true statement about them and is accepted as a no-op rather than an error.
|
|
165
226
|
// Anything else has no device grant at all and must fail loudly.
|
|
166
227
|
if (device && !CODEX_NAMES.has(provider) && !DEVICE_NATIVE_PROVIDERS.has(provider)) {
|
|
167
|
-
throw new CliUsageError(
|
|
228
|
+
throw new CliUsageError("--device is not supported for this provider", USAGE);
|
|
168
229
|
}
|
|
230
|
+
if (addAccount !== undefined && (reauth || CODEX_NAMES.has(provider))) throw new CliUsageError("--add-account is only valid for fresh provider OAuth login", USAGE);
|
|
231
|
+
if (openBrowser !== undefined && (device || DEVICE_NATIVE_PROVIDERS.has(provider))) throw new CliUsageError("--open-browser is not supported for device login", USAGE);
|
|
232
|
+
if (!CODEX_NAMES.has(provider) && id && !reauth) throw new CliUsageError("--id is only valid with --reauth for provider OAuth accounts", USAGE);
|
|
169
233
|
// Only resolve when --code was actually given: a plain `ocx account login`
|
|
170
234
|
// opens the browser flow and polls, and must not block on stdin.
|
|
171
235
|
const code = await resolveCode(suppliedCode, deps, false);
|
|
236
|
+
const pinned = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
|
|
172
237
|
|
|
173
238
|
if (method) {
|
|
174
|
-
const start = await runtimeRequest<
|
|
175
|
-
method: "POST", body: JSON.stringify({ provider: "kiro", method }),
|
|
176
|
-
},
|
|
239
|
+
const start = publicLoginStart(await runtimeRequest<unknown>("/api/oauth/login", {
|
|
240
|
+
method: "POST", redirect: "error", body: JSON.stringify({ provider: "kiro", method }),
|
|
241
|
+
}, pinned), true);
|
|
177
242
|
if (!wantsJson) {
|
|
178
243
|
const block = formatKiroDeviceInstructions(start);
|
|
179
244
|
if (block) writeStdoutFully(`${block}\n`);
|
|
@@ -182,7 +247,7 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
182
247
|
if (!start.flowId) throw new CliUsageError("Kiro device login did not return a flow id");
|
|
183
248
|
for (let attempt = 0; attempt < 450; attempt++) {
|
|
184
249
|
await Bun.sleep(2_000);
|
|
185
|
-
const state = await runtimeRequest<
|
|
250
|
+
const state = publicLoginStart(await runtimeRequest<unknown>(`/api/oauth/status?provider=kiro&flowId=${encodeURIComponent(start.flowId)}`, { redirect: "error" }, pinned), true);
|
|
186
251
|
if (state.state === "done") {
|
|
187
252
|
printData(state, wantsJson, ["Logged in to kiro.", ...(state.warning ? [`Warning: ${state.warning}`] : [])]);
|
|
188
253
|
return;
|
|
@@ -195,31 +260,33 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
195
260
|
}
|
|
196
261
|
|
|
197
262
|
if (CODEX_NAMES.has(provider)) {
|
|
198
|
-
const start = await runtimeRequest<
|
|
199
|
-
method: "POST",
|
|
263
|
+
const start = publicLoginStart(await runtimeRequest<unknown>("/api/codex-auth/login", {
|
|
264
|
+
method: "POST", redirect: "error",
|
|
200
265
|
body: JSON.stringify({
|
|
201
266
|
...(id ? { id } : {}),
|
|
202
267
|
...(reauth ? { reauth: true } : {}),
|
|
203
268
|
...(device ? { device: true } : {}),
|
|
269
|
+
...(openBrowser === undefined ? {} : { openBrowser }),
|
|
204
270
|
}),
|
|
205
|
-
},
|
|
271
|
+
}, pinned));
|
|
206
272
|
if (!wantsJson) {
|
|
207
273
|
// One atomic pre-poll block, flushed synchronously so a piped parent
|
|
208
274
|
// reads the URL before the polling window starts (#1007).
|
|
209
|
-
|
|
275
|
+
writeLoginInstructions([
|
|
210
276
|
start.url ? `Open this URL to sign in:\n${start.url}` : "",
|
|
211
277
|
start.deviceCode ? `Device code: ${start.deviceCode}` : "",
|
|
212
278
|
start.instructions ?? "",
|
|
213
279
|
start.browserLaunch === "failed" ? BROWSER_LAUNCH_FAILED_HINT : "",
|
|
214
280
|
start.flowId ? `Flow: ${start.flowId}` : "",
|
|
215
|
-
]
|
|
216
|
-
if (block) writeStdoutFully(`${block}\n`);
|
|
281
|
+
]);
|
|
217
282
|
}
|
|
283
|
+
if (code && !start.flowId) throw new Error("Code submission requires a returned flow id");
|
|
218
284
|
if (code && start.flowId) {
|
|
219
|
-
await runtimeRequest("/api/codex-auth/login/code", {
|
|
220
|
-
method: "POST",
|
|
285
|
+
const submitted = await runtimeRequest("/api/codex-auth/login/code", {
|
|
286
|
+
method: "POST", redirect: "error",
|
|
221
287
|
body: JSON.stringify({ flowId: start.flowId, input: code }),
|
|
222
|
-
},
|
|
288
|
+
}, pinned);
|
|
289
|
+
if (loginObject(submitted).ok !== true) throw new Error("Code submission was not accepted");
|
|
223
290
|
}
|
|
224
291
|
if (noWait) {
|
|
225
292
|
printData({ ...start, modelSelection: modelSelectionNextSteps("openai", true) }, wantsJson, modelSelectionGuidance("openai", true));
|
|
@@ -233,10 +300,11 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
233
300
|
const maxAttempts = device ? 480 : 150;
|
|
234
301
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
235
302
|
await Bun.sleep(2_000);
|
|
236
|
-
const state = await runtimeRequest<
|
|
303
|
+
const state = publicLoginStatus(await runtimeRequest<unknown>(
|
|
237
304
|
`/api/codex-auth/login-status?flowId=${encodeURIComponent(start.flowId)}${id ? `&accountId=${encodeURIComponent(id)}` : ""}${reauth ? "&reauth=1" : ""}`,
|
|
238
|
-
{},
|
|
239
|
-
);
|
|
305
|
+
{ redirect: "error" }, pinned,
|
|
306
|
+
));
|
|
307
|
+
if (start.flowId) state.flowId = start.flowId;
|
|
240
308
|
if (state.status === "done") {
|
|
241
309
|
if (state.validationPending === true) {
|
|
242
310
|
printData({ ...state, recoveryCommand: "ocx gui", recoveryAction: "After quota recovers, click Refresh quotas in the dashboard Codex account pool." }, wantsJson, [
|
|
@@ -247,33 +315,32 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
247
315
|
printData({ ...state, modelSelection: modelSelectionNextSteps("openai") }, wantsJson, [`Logged in${state.email ? ` as ${String(state.email)}` : ""}.`, ...modelSelectionGuidance("openai")]);
|
|
248
316
|
}
|
|
249
317
|
if (!wantsJson) warnIfCodexCatalogRefreshPending(state);
|
|
250
|
-
return;
|
|
318
|
+
return state.validationPending === true || state.catalogRefreshPending === true ? 1 : 0;
|
|
251
319
|
}
|
|
252
|
-
if (state.status === "error" || state.status === "expired") {
|
|
253
|
-
throw new
|
|
320
|
+
if (state.status === "error" || state.status === "expired" || state.status === "cancelled") {
|
|
321
|
+
throw new Error("Login failed or expired");
|
|
254
322
|
}
|
|
255
323
|
}
|
|
256
324
|
throw new CliUsageError("login timed out");
|
|
257
325
|
}
|
|
258
326
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}, deps);
|
|
327
|
+
const start = publicLoginStart(await runtimeRequest<unknown>("/api/oauth/login", {
|
|
328
|
+
method: "POST", redirect: "error",
|
|
329
|
+
body: JSON.stringify({ provider, addAccount: addAccount ?? !reauth, ...(openBrowser === undefined ? {} : { openBrowser }), ...(reauth && id ? { accountId: id, reauth: true } : {}) }),
|
|
330
|
+
}, pinned));
|
|
264
331
|
if (!wantsJson) {
|
|
265
|
-
|
|
332
|
+
writeLoginInstructions([
|
|
266
333
|
start.url ? `Open this URL to sign in:\n${start.url}` : "",
|
|
267
334
|
start.instructions ?? "",
|
|
268
335
|
start.deviceCode ? `Device code: ${start.deviceCode}` : "",
|
|
269
|
-
]
|
|
270
|
-
if (block) writeStdoutFully(`${block}\n`);
|
|
336
|
+
]);
|
|
271
337
|
}
|
|
272
338
|
if (code) {
|
|
273
|
-
await runtimeRequest("/api/oauth/login/code", {
|
|
274
|
-
method: "POST",
|
|
339
|
+
const submitted = await runtimeRequest("/api/oauth/login/code", {
|
|
340
|
+
method: "POST", redirect: "error",
|
|
275
341
|
body: JSON.stringify({ provider, input: code }),
|
|
276
|
-
},
|
|
342
|
+
}, pinned);
|
|
343
|
+
if (loginObject(submitted).ok !== true) throw new Error("Code submission was not accepted");
|
|
277
344
|
}
|
|
278
345
|
if (noWait) {
|
|
279
346
|
printData({ ...start, modelSelection: modelSelectionNextSteps(provider, true) }, wantsJson, modelSelectionGuidance(provider, true));
|
|
@@ -281,8 +348,10 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
281
348
|
}
|
|
282
349
|
for (let attempt = 0; attempt < 100; attempt++) {
|
|
283
350
|
await Bun.sleep(2_000);
|
|
284
|
-
const
|
|
285
|
-
if (
|
|
351
|
+
const raw = await runtimeRequest<Record<string, unknown>>(`/api/oauth/status?provider=${encodeURIComponent(provider)}`, { redirect: "error" }, pinned);
|
|
352
|
+
if (loginObject(raw).error) throw new Error("Provider login failed");
|
|
353
|
+
const state = publicLoginStatus(raw);
|
|
354
|
+
if (state.done === true && state.loggedIn !== true) throw new Error("Provider login ended without authentication");
|
|
286
355
|
if (state.loggedIn === true) {
|
|
287
356
|
printData({ ...state, modelSelection: modelSelectionNextSteps(provider) }, wantsJson, [`Logged in to ${provider}.`, ...modelSelectionGuidance(provider)]);
|
|
288
357
|
return;
|
|
@@ -413,7 +482,7 @@ async function grokResetCoupons(argv: string[], deps: RuntimeApiDeps): Promise<v
|
|
|
413
482
|
|
|
414
483
|
export async function handleAccountAuthCommand(sub: string, argv: string[], deps: RuntimeApiDeps = {}): Promise<number | null> {
|
|
415
484
|
let action: (() => Promise<void>) | undefined;
|
|
416
|
-
if (sub === "login" || sub === "reauth")
|
|
485
|
+
if (sub === "login" || sub === "reauth") return runCatalogAction(() => login(sub === "reauth" ? [...argv, "--reauth"] : argv, deps));
|
|
417
486
|
else if (sub === "code") action = () => code(argv, deps);
|
|
418
487
|
else if (sub === "cancel") action = () => cancel(argv, deps);
|
|
419
488
|
else if (sub === "reset-credits") action = () => resetCredits(argv, deps);
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/** Public API-key quota projection; private probe identity stays on the server. */
|
|
2
|
+
import { parseQuotaFailureCode, type AccountQuotaFields, type ProviderQuota, type ProviderQuotaWindow, type ProviderQuotaCreditsUsd } from "../providers/quota-types";
|
|
3
|
+
import { terminalSafeText } from "./runtime-api";
|
|
4
|
+
import type { AccountRow, FamilyRows } from "./account-api";
|
|
5
|
+
|
|
6
|
+
function record(value: unknown): Record<string, unknown> {
|
|
7
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Malformed API-key quota response.");
|
|
8
|
+
return value as Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
function finite(value: unknown): number {
|
|
11
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error("Malformed API-key quota response.");
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
function optionalString(row: Record<string, unknown>, key: string): string | undefined {
|
|
15
|
+
if (row[key] === undefined) return undefined;
|
|
16
|
+
if (typeof row[key] !== "string") throw new Error("Malformed API-key quota response.");
|
|
17
|
+
return row[key];
|
|
18
|
+
}
|
|
19
|
+
function optionalBoolean(row: Record<string, unknown>, key: string): boolean | undefined {
|
|
20
|
+
if (row[key] === undefined) return undefined;
|
|
21
|
+
if (typeof row[key] !== "boolean") throw new Error("Malformed API-key quota response.");
|
|
22
|
+
return row[key];
|
|
23
|
+
}
|
|
24
|
+
function windowProjection(value: unknown): ProviderQuotaWindow {
|
|
25
|
+
const row = record(value);
|
|
26
|
+
const label = optionalString(row, "label");
|
|
27
|
+
if (label === undefined) throw new Error("Malformed API-key quota response.");
|
|
28
|
+
const result: ProviderQuotaWindow = { label, percent: finite(row.percent) };
|
|
29
|
+
if (row.resetAt !== undefined) result.resetAt = finite(row.resetAt);
|
|
30
|
+
if (row.passiveObservedAt !== undefined) result.passiveObservedAt = finite(row.passiveObservedAt);
|
|
31
|
+
if (row.scope !== undefined) {
|
|
32
|
+
if (row.scope !== "model") throw new Error("Malformed API-key quota response.");
|
|
33
|
+
result.scope = row.scope;
|
|
34
|
+
}
|
|
35
|
+
if (row.rejected !== undefined) {
|
|
36
|
+
if (row.rejected !== true) throw new Error("Malformed API-key quota response.");
|
|
37
|
+
result.rejected = true;
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
function creditsProjection(value: unknown): ProviderQuotaCreditsUsd {
|
|
42
|
+
const row = record(value);
|
|
43
|
+
const result: ProviderQuotaCreditsUsd = {
|
|
44
|
+
used: finite(row.used), limit: finite(row.limit), remaining: finite(row.remaining), percent: finite(row.percent),
|
|
45
|
+
};
|
|
46
|
+
if (row.expiresAt !== undefined) result.expiresAt = finite(row.expiresAt);
|
|
47
|
+
const unlimited = optionalBoolean(row, "unlimited");
|
|
48
|
+
if (unlimited !== undefined) result.unlimited = unlimited;
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
function quotaProjection(value: unknown): ProviderQuota | null {
|
|
52
|
+
if (value === null) return null;
|
|
53
|
+
const row = record(value);
|
|
54
|
+
const result: ProviderQuota = { updatedAt: finite(row.updatedAt) };
|
|
55
|
+
for (const key of ["fiveHourPercent", "fiveHourResetAt", "weeklyPercent", "weeklyResetAt", "monthlyPercent", "monthlyResetAt", "kiroCreditsUsed", "kiroCreditsLimit"] as const) {
|
|
56
|
+
if (row[key] !== undefined) result[key] = finite(row[key]);
|
|
57
|
+
}
|
|
58
|
+
if (row.customWindows !== undefined) {
|
|
59
|
+
if (!Array.isArray(row.customWindows)) throw new Error("Malformed API-key quota response.");
|
|
60
|
+
result.customWindows = row.customWindows.map(windowProjection);
|
|
61
|
+
}
|
|
62
|
+
if (row.creditsUsd !== undefined) result.creditsUsd = creditsProjection(row.creditsUsd);
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Only opt-in reads use the strict DTO boundary; legacy cheap lists retain their contract. */
|
|
67
|
+
export function projectApiKeyQuotaRows(value: unknown, provider: string): FamilyRows {
|
|
68
|
+
const body = record(value);
|
|
69
|
+
if (!Array.isArray(body.keys) || (body.activeId !== null && typeof body.activeId !== "string")) {
|
|
70
|
+
throw new Error("Malformed API-key quota response.");
|
|
71
|
+
}
|
|
72
|
+
const activeId = body.activeId;
|
|
73
|
+
const rows: AccountRow[] = body.keys.map(value => {
|
|
74
|
+
const row = record(value);
|
|
75
|
+
const id = optionalString(row, "id");
|
|
76
|
+
if (!id) throw new Error("Malformed API-key quota response.");
|
|
77
|
+
const quotaMode = row.quotaMode;
|
|
78
|
+
if (quotaMode === undefined) throw new Error("API-key quota is unverified: server did not report quotaMode. Update the proxy and retry.");
|
|
79
|
+
if (quotaMode !== "probe" && quotaMode !== "passive" && quotaMode !== "unsupported") throw new Error("Malformed API-key quota response.");
|
|
80
|
+
const fields: AccountQuotaFields = { quotaMode };
|
|
81
|
+
if (row.quota !== undefined) fields.quota = quotaProjection(row.quota);
|
|
82
|
+
const unavailable = optionalBoolean(row, "quotaUnavailable");
|
|
83
|
+
if (unavailable !== undefined) fields.quotaUnavailable = unavailable;
|
|
84
|
+
if (row.quotaFailure !== undefined) {
|
|
85
|
+
const failure = parseQuotaFailureCode(row.quotaFailure);
|
|
86
|
+
if (!failure) throw new Error("Malformed API-key quota response.");
|
|
87
|
+
if (unavailable === true) fields.quotaFailure = failure;
|
|
88
|
+
}
|
|
89
|
+
const masked = optionalString(row, "masked");
|
|
90
|
+
return { provider, type: "api-key", id, label: optionalString(row, "label") ?? masked,
|
|
91
|
+
masked, active: optionalBoolean(row, "active") ?? id === activeId, ...fields };
|
|
92
|
+
});
|
|
93
|
+
return { rows, activeId, status: 200 };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function apiKeyQuotaText(row: AccountRow): string {
|
|
97
|
+
const mode = row.quotaMode;
|
|
98
|
+
if (!mode) return "unverified";
|
|
99
|
+
if (mode === "unsupported") return "unsupported";
|
|
100
|
+
if (row.quotaUnavailable) return `${mode}: unavailable${row.quotaFailure ? ` (${row.quotaFailure})` : ""}`;
|
|
101
|
+
const quota = row.quota;
|
|
102
|
+
if (!quota) return `${mode}: not measured`;
|
|
103
|
+
const parts: string[] = [];
|
|
104
|
+
for (const [label, percent] of [["5h", quota.fiveHourPercent], ["wk", quota.weeklyPercent], ["mo", quota.monthlyPercent]] as const) {
|
|
105
|
+
if (percent !== undefined) parts.push(`${label} ${percent}%`);
|
|
106
|
+
}
|
|
107
|
+
for (const window of quota.customWindows ?? []) {
|
|
108
|
+
// Labels are upstream text: keep terminal controls out of the human table.
|
|
109
|
+
const label = terminalSafeText(window.label);
|
|
110
|
+
parts.push(`${label} ${window.percent}%${window.rejected ? " (rejected)" : ""}`);
|
|
111
|
+
}
|
|
112
|
+
if (quota.creditsUsd) {
|
|
113
|
+
const c = quota.creditsUsd;
|
|
114
|
+
parts.push(`USD ${c.used}/${c.unlimited ? "unlimited" : c.limit} used; ${c.remaining} remaining (${c.percent}%)`);
|
|
115
|
+
}
|
|
116
|
+
if (quota.kiroCreditsUsed !== undefined || quota.kiroCreditsLimit !== undefined) parts.push(`credits ${quota.kiroCreditsUsed ?? "?"}/${quota.kiroCreditsLimit ?? "?"}`);
|
|
117
|
+
return `${mode}: ${parts.join("; ") || "not measured"}`;
|
|
118
|
+
}
|