@iislee/opencodex 2.31.0 → 2.35.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/README.md +3 -3
- package/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-Be1sDpWw.css +1 -0
- package/gui/dist/assets/index-D4OKgUV4.js +114 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/adapters/anthropic.ts +104 -15
- package/src/adapters/base.ts +29 -1
- package/src/adapters/command-code.ts +42 -6
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +69 -18
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +290 -0
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +14 -0
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +2 -2
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -2
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +68 -14
- package/src/adapters/cursor/request-builder.ts +40 -10
- package/src/adapters/cursor/tool-definitions.ts +14 -4
- package/src/adapters/cursor/tool-result-normalize.ts +28 -5
- package/src/adapters/cursor/types.ts +20 -0
- package/src/adapters/cursor.ts +209 -38
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +173 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google.ts +136 -24
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +27 -0
- package/src/adapters/kiro-tools.ts +55 -16
- package/src/adapters/kiro.ts +182 -23
- package/src/adapters/openai-chat.ts +300 -242
- package/src/adapters/openai-responses.ts +353 -27
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +4 -3
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/adapters/xai-web-search.ts +17 -16
- package/src/bridge.ts +90 -37
- package/src/chat/inbound.ts +14 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/context-windows.ts +16 -9
- package/src/claude/outbound.ts +24 -11
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +116 -19
- package/src/cli/account-extended.ts +262 -29
- package/src/cli/account-main.ts +12 -12
- package/src/cli/account.ts +69 -15
- package/src/cli/agent.ts +8 -1
- package/src/cli/alias.ts +66 -0
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +496 -0
- package/src/cli/claude-desktop.ts +31 -11
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +256 -28
- package/src/cli/doctor.ts +126 -14
- package/src/cli/help.ts +17 -6
- package/src/cli/index.ts +39 -9
- package/src/cli/init.ts +1 -0
- package/src/cli/inspect.ts +230 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +26 -10
- package/src/cli/observe.ts +31 -8
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/provider.ts +2 -1
- package/src/cli/registry.ts +49 -10
- package/src/cli/runtime-api.ts +51 -7
- package/src/cli/status.ts +41 -1
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +184 -0
- package/src/cli/v2.ts +34 -10
- package/src/cli/version-skew.ts +46 -0
- package/src/clients/config-export.ts +85 -3
- package/src/clients/effective-status.ts +30 -2
- package/src/clients/probes/cline.ts +81 -0
- package/src/codex/account-label.ts +21 -0
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +83 -7
- package/src/codex/auth-api.ts +50 -10
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/autostart-health.ts +16 -0
- package/src/codex/catalog/aggregation.ts +18 -0
- package/src/codex/catalog/effort.ts +18 -3
- package/src/codex/catalog/metadata.ts +27 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +72 -27
- package/src/codex/catalog/provider-fetch.ts +187 -31
- package/src/codex/catalog/sync.ts +131 -5
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +27 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +355 -0
- package/src/codex/history-job.ts +32 -3
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +5 -5
- package/src/codex/history-provider.ts +825 -247
- package/src/codex/history-worker.ts +8 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +63 -22
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +91 -10
- package/src/codex/routing.ts +9 -0
- package/src/codex/shim.ts +56 -3
- package/src/codex/subagent-model-fallback.ts +104 -25
- package/src/codex/transition-state.ts +116 -8
- package/src/combos/failover.ts +27 -0
- package/src/combos/types.ts +10 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +121 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +309 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +206 -824
- package/src/generated/compatibility-version.json +373 -213
- package/src/images/loop.ts +5 -4
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +160 -0
- package/src/integrations/ownership.ts +46 -2
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/state.ts +79 -7
- package/src/integrations/writer.ts +26 -2
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +36 -3
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/lib/process-control.ts +2 -1
- package/src/lib/service-secrets.ts +15 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/callback-server.ts +22 -2
- package/src/oauth/cline.ts +203 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/health.ts +1 -1
- package/src/oauth/index.ts +65 -6
- package/src/oauth/key-providers.ts +10 -1
- package/src/oauth/kimi.ts +9 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/local-token-detect.ts +2 -0
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +14 -5
- package/src/oauth/types.ts +15 -0
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +28 -0
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/label.ts +34 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +65 -7
- package/src/providers/registry.ts +184 -69
- package/src/providers/slug-codec.ts +52 -0
- package/src/providers/xai-transport.ts +21 -0
- package/src/reasoning-effort.ts +19 -2
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +157 -39
- package/src/responses/namespace-tool-compat.ts +84 -4
- package/src/responses/parser.ts +7 -2
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/responses/schema.ts +5 -1
- package/src/responses/state.ts +94 -4
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/responses/turn-termination.ts +107 -0
- package/src/router.ts +40 -2
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +27 -7
- package/src/server/chat-completions.ts +24 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/index.ts +179 -32
- package/src/server/lifecycle.ts +52 -1
- package/src/server/local-management-read-client.ts +1 -1
- package/src/server/local-provider-reload-client.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +45 -23
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +59 -12
- package/src/server/management/context.ts +10 -1
- package/src/server/management/logs-usage-routes.ts +30 -24
- package/src/server/management/model-routes.ts +274 -4
- package/src/server/management/model-rows.ts +4 -0
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/oauth-account-routes.ts +10 -4
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/provider-routes.ts +81 -11
- package/src/server/management/route-registry.ts +332 -0
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/management-api.ts +18 -16
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +29 -5
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +51 -0
- package/src/server/request-log.ts +70 -16
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/collaboration.ts +37 -3
- package/src/server/responses/combo-stream-preflight.ts +178 -0
- package/src/server/responses/compact.ts +6 -21
- package/src/server/responses/core.ts +810 -208
- package/src/server/responses/empty-completion-guard.ts +63 -6
- package/src/server/responses/fetch-helpers.ts +17 -102
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses/ws-upstream.ts +75 -2
- package/src/server/responses-custom-tool-repair.ts +87 -6
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/responses-undeclared-tool-guard.ts +241 -18
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/service.ts +42 -4
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +88 -23
- package/src/types/config.ts +66 -0
- package/src/types/provider.ts +69 -0
- package/src/types/request.ts +17 -4
- package/src/types/tools.ts +116 -11
- package/src/types.ts +3 -1
- package/src/update/index.ts +5 -4
- package/src/update/job.ts +3 -1
- package/src/update/transactional-install.mjs +8 -1
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +63 -12
- package/src/usage/summary.ts +211 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/executor.ts +10 -3
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-BF38heuV.js +0 -104
- package/gui/dist/assets/index-DMiI18Kv.css +0 -1
|
@@ -484,6 +484,29 @@ export function primeConfigPath(env: OpencodeLaunchEnv = process.env, home: stri
|
|
|
484
484
|
return join(primeAgentDir(env, home), "models.json");
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
+
export function clineHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
488
|
+
const override = env.CLINE_DATA_DIR?.trim();
|
|
489
|
+
if (override) return absoluteClientPath(override, home, "CLINE_DATA_DIR");
|
|
490
|
+
|
|
491
|
+
const vscodeDir = process.platform === "darwin"
|
|
492
|
+
? join(home, "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings")
|
|
493
|
+
: process.platform === "win32"
|
|
494
|
+
? join(env.APPDATA || join(home, "AppData", "Roaming"), "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings")
|
|
495
|
+
: join(home, ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings");
|
|
496
|
+
|
|
497
|
+
if (existsSync(vscodeDir)) return vscodeDir;
|
|
498
|
+
|
|
499
|
+
const cliSettingsDir = join(home, ".cline", "data", "settings");
|
|
500
|
+
if (existsSync(cliSettingsDir) || existsSync(join(home, ".cline"))) return cliSettingsDir;
|
|
501
|
+
|
|
502
|
+
return vscodeDir;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** Cline's canonical providers configuration file. */
|
|
506
|
+
export function clineConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
|
|
507
|
+
return join(clineHomeDir(env, home), "providers.json");
|
|
508
|
+
}
|
|
509
|
+
|
|
487
510
|
/**
|
|
488
511
|
* One proxy-routed model destined for a client config. Deliberately narrower than
|
|
489
512
|
* `CatalogModel` so a serializer cannot reach for a field that does not survive the
|
|
@@ -526,7 +549,8 @@ export type ExportClientId =
|
|
|
526
549
|
| "dsh"
|
|
527
550
|
| "mcode"
|
|
528
551
|
| "zcode"
|
|
529
|
-
| "prime"
|
|
552
|
+
| "prime"
|
|
553
|
+
| "cline";
|
|
530
554
|
|
|
531
555
|
export interface ExportClientSpec {
|
|
532
556
|
id: ExportClientId;
|
|
@@ -725,10 +749,10 @@ export function buildOpencodeProviderBlockFromCatalog(
|
|
|
725
749
|
* models produce identical bytes. Stability matters because the GUI shows a diffable
|
|
726
750
|
* preview and agents may checksum the payload.
|
|
727
751
|
*/
|
|
728
|
-
export function normalizeExportModels(models: readonly ExportModel[]): ExportModel[] {
|
|
752
|
+
export function normalizeExportModels(models: readonly ExportModel[] = []): ExportModel[] {
|
|
729
753
|
const seen = new Set<string>();
|
|
730
754
|
const unique: ExportModel[] = [];
|
|
731
|
-
for (const model of models) {
|
|
755
|
+
for (const model of models ?? []) {
|
|
732
756
|
if (seen.has(model.namespaced)) continue;
|
|
733
757
|
seen.add(model.namespaced);
|
|
734
758
|
unique.push(model);
|
|
@@ -1379,6 +1403,42 @@ function buildZcodeClientConfig(ctx: ExportContext): ZcodeGeneratedConfig {
|
|
|
1379
1403
|
};
|
|
1380
1404
|
}
|
|
1381
1405
|
|
|
1406
|
+
export interface ClineProviderSettings {
|
|
1407
|
+
provider: string;
|
|
1408
|
+
baseUrl: string;
|
|
1409
|
+
apiKey?: string;
|
|
1410
|
+
model?: string;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
export interface ClineProviderBlock {
|
|
1414
|
+
settings: ClineProviderSettings;
|
|
1415
|
+
tokenSource?: string;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
export interface ClineGeneratedConfig {
|
|
1419
|
+
version: number;
|
|
1420
|
+
providers: Record<string, ClineProviderBlock>;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
function buildClineClientConfig(ctx: ExportContext): ClineGeneratedConfig {
|
|
1424
|
+
const models = normalizeExportModels(ctx.models);
|
|
1425
|
+
const defaultModel = models[0]?.namespaced ?? "opencodex";
|
|
1426
|
+
return {
|
|
1427
|
+
version: 1,
|
|
1428
|
+
providers: {
|
|
1429
|
+
[OPENCODE_PROVIDER_ID]: {
|
|
1430
|
+
settings: {
|
|
1431
|
+
provider: "openai-compatible",
|
|
1432
|
+
baseUrl: ctx.baseUrl.replace(/\/v1\/?$/, "") + "/v1",
|
|
1433
|
+
apiKey: LOOPBACK_API_KEY_PLACEHOLDER,
|
|
1434
|
+
model: defaultModel,
|
|
1435
|
+
},
|
|
1436
|
+
tokenSource: "manual",
|
|
1437
|
+
},
|
|
1438
|
+
},
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1382
1442
|
/**
|
|
1383
1443
|
* Per-client model counts, read back off the SERIALIZED document rather than
|
|
1384
1444
|
* recomputed from the input rows: `modelsWithoutLimits` drives a GUI line about
|
|
@@ -1390,6 +1450,11 @@ function summarizeOpencode(document: unknown): { modelCount: number; modelsWitho
|
|
|
1390
1450
|
return { modelCount: models.length, modelsWithoutLimits: models.filter(model => !model.limit).length };
|
|
1391
1451
|
}
|
|
1392
1452
|
|
|
1453
|
+
function summarizeCline(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
|
|
1454
|
+
const provider = (document as ClineGeneratedConfig | undefined)?.providers?.[OPENCODE_PROVIDER_ID];
|
|
1455
|
+
return { modelCount: provider ? 1 : 0, modelsWithoutLimits: 0 };
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1393
1458
|
function summarizePi(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
|
|
1394
1459
|
const models = (document as PiGeneratedConfig | undefined)?.providers?.[OPENCODE_PROVIDER_ID]?.models ?? [];
|
|
1395
1460
|
return { modelCount: models.length, modelsWithoutLimits: models.filter(model => model.contextWindow === undefined).length };
|
|
@@ -1522,6 +1587,11 @@ function buildPrimeContribution(ctx: ExportContext): ManagedContribution {
|
|
|
1522
1587
|
return singleFragment("prime", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]);
|
|
1523
1588
|
}
|
|
1524
1589
|
|
|
1590
|
+
function buildClineContribution(ctx: ExportContext): ManagedContribution {
|
|
1591
|
+
const doc = buildClineClientConfig(ctx);
|
|
1592
|
+
return singleFragment("cline", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]);
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1525
1595
|
export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
|
|
1526
1596
|
opencode: {
|
|
1527
1597
|
id: "opencode",
|
|
@@ -1675,6 +1745,18 @@ export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
|
|
|
1675
1745
|
// from this initial loopback-only integration — same stance as OMP's.
|
|
1676
1746
|
loopbackOnly: true,
|
|
1677
1747
|
},
|
|
1748
|
+
cline: {
|
|
1749
|
+
id: "cline",
|
|
1750
|
+
filename: "providers.json",
|
|
1751
|
+
destination: env => clineConfigPath(env),
|
|
1752
|
+
apiKeyEnv: "",
|
|
1753
|
+
exportHint: "Cline reads a non-secret placeholder from providers.json; loopback needs no key.",
|
|
1754
|
+
build: buildClineClientConfig,
|
|
1755
|
+
format: "json",
|
|
1756
|
+
summarize: summarizeCline,
|
|
1757
|
+
buildContribution: buildClineContribution,
|
|
1758
|
+
loopbackOnly: true,
|
|
1759
|
+
},
|
|
1678
1760
|
};
|
|
1679
1761
|
|
|
1680
1762
|
export const EXPORT_CLIENT_IDS: readonly ExportClientId[] = Object.keys(EXPORT_CLIENTS) as ExportClientId[];
|
|
@@ -18,10 +18,11 @@ import { probePi } from "./probes/pi";
|
|
|
18
18
|
import { probeGrok } from "./probes/grok";
|
|
19
19
|
import { probeOpencode } from "./probes/opencode";
|
|
20
20
|
import { probeAgy } from "./probes/agy";
|
|
21
|
+
import { probeCline } from "./probes/cline";
|
|
21
22
|
import { readCcSwitchCurrentProfiles, type CcSwitchProfile } from "./probes/cc-switch";
|
|
22
23
|
import { readPaseoProviderCommands, type PaseoProviderCommand } from "./probes/paseo";
|
|
23
24
|
|
|
24
|
-
export type ClientId = "claude" | "codex" | "pi" | "grok" | "opencode" | "agy";
|
|
25
|
+
export type ClientId = "claude" | "codex" | "pi" | "grok" | "opencode" | "agy" | "cline";
|
|
25
26
|
|
|
26
27
|
export type ClientVerdict = "ocx" | "direct" | "mixed" | "missing" | "unknown";
|
|
27
28
|
|
|
@@ -70,6 +71,7 @@ export interface EffectiveStatusOpts {
|
|
|
70
71
|
grok?: typeof probeGrok;
|
|
71
72
|
opencode?: typeof probeOpencode;
|
|
72
73
|
agy?: typeof probeAgy;
|
|
74
|
+
cline?: typeof probeCline;
|
|
73
75
|
ccSwitch?: typeof readCcSwitchCurrentProfiles;
|
|
74
76
|
paseo?: typeof readPaseoProviderCommands;
|
|
75
77
|
};
|
|
@@ -82,6 +84,7 @@ const CLIENT_LABELS: Record<ClientId, string> = {
|
|
|
82
84
|
grok: "Grok Build",
|
|
83
85
|
opencode: "OpenCode",
|
|
84
86
|
agy: "Antigravity (agy)",
|
|
87
|
+
cline: "Cline",
|
|
85
88
|
};
|
|
86
89
|
|
|
87
90
|
/** Map client id → CC Switch `app_type` column (when present). */
|
|
@@ -90,6 +93,7 @@ const CC_SWITCH_APP: Partial<Record<ClientId, string>> = {
|
|
|
90
93
|
codex: "codex",
|
|
91
94
|
grok: "grokbuild",
|
|
92
95
|
opencode: "opencode",
|
|
96
|
+
cline: "cline",
|
|
93
97
|
};
|
|
94
98
|
|
|
95
99
|
/** Map client id → Paseo `agents.providers` key. */
|
|
@@ -99,6 +103,7 @@ const PASEO_PROVIDER: Partial<Record<ClientId, string>> = {
|
|
|
99
103
|
agy: "antigravity-acp",
|
|
100
104
|
claude: "claude",
|
|
101
105
|
codex: "codex",
|
|
106
|
+
cline: "cline",
|
|
102
107
|
};
|
|
103
108
|
|
|
104
109
|
export interface ProxyTarget {
|
|
@@ -220,13 +225,14 @@ export async function readClientsEffectiveStatus(
|
|
|
220
225
|
const grokFn = opts.probes?.grok ?? probeGrok;
|
|
221
226
|
const opencodeFn = opts.probes?.opencode ?? probeOpencode;
|
|
222
227
|
const agyFn = opts.probes?.agy ?? probeAgy;
|
|
228
|
+
const clineFn = opts.probes?.cline ?? probeCline;
|
|
223
229
|
const ccSwitchFn = opts.probes?.ccSwitch ?? readCcSwitchCurrentProfiles;
|
|
224
230
|
const paseoFn = opts.probes?.paseo ?? readPaseoProviderCommands;
|
|
225
231
|
|
|
226
232
|
const emptyProfiles: CcSwitchProfile[] = [];
|
|
227
233
|
const emptyPaseo: PaseoProviderCommand[] = [];
|
|
228
234
|
|
|
229
|
-
const [ccProfiles, paseoCommands, claude, codex, pi, grok, opencode, agy] = await Promise.all([
|
|
235
|
+
const [ccProfiles, paseoCommands, claude, codex, pi, grok, opencode, agy, cline] = await Promise.all([
|
|
230
236
|
safeProbeAsync("cc-switch", () => Promise.resolve(ccSwitchFn({ home })), emptyProfiles),
|
|
231
237
|
safeProbeAsync("paseo", () => Promise.resolve(paseoFn({ home })), emptyPaseo),
|
|
232
238
|
safeProbeAsync("claude", () => Promise.resolve(claudeFn({ home })), {
|
|
@@ -247,6 +253,9 @@ export async function readClientsEffectiveStatus(
|
|
|
247
253
|
safeProbeAsync("agy", () => Promise.resolve(agyFn({ home })), {
|
|
248
254
|
present: false, baseUrl: null, model: null, configPaths: [] as string[], notes: ["probe failed"], binary: null,
|
|
249
255
|
}),
|
|
256
|
+
safeProbeAsync("cline", () => Promise.resolve(clineFn({ home })), {
|
|
257
|
+
present: false, baseUrl: null, model: null, configPaths: [] as string[], notes: ["probe failed"], binary: null,
|
|
258
|
+
}),
|
|
250
259
|
]);
|
|
251
260
|
|
|
252
261
|
const clients: ClientEffectiveStatus[] = [];
|
|
@@ -372,6 +381,25 @@ export async function readClientsEffectiveStatus(
|
|
|
372
381
|
});
|
|
373
382
|
}
|
|
374
383
|
|
|
384
|
+
// Cline
|
|
385
|
+
{
|
|
386
|
+
const raw = cline;
|
|
387
|
+
const { verdict, viaOcx } = verdictFromBaseUrl(raw.baseUrl, proxy, { present: raw.present });
|
|
388
|
+
clients.push({
|
|
389
|
+
id: "cline",
|
|
390
|
+
label: CLIENT_LABELS.cline,
|
|
391
|
+
present: raw.present,
|
|
392
|
+
viaOcx,
|
|
393
|
+
verdict: raw.present ? (raw.baseUrl ? verdict : "unknown") : "missing",
|
|
394
|
+
baseUrl: raw.baseUrl,
|
|
395
|
+
model: raw.model,
|
|
396
|
+
configPaths: raw.configPaths,
|
|
397
|
+
launcher: paseoLauncher("cline", paseoCommands) ?? (raw.binary ? "cline" : null),
|
|
398
|
+
switcher: switcherFor("cline", ccProfiles),
|
|
399
|
+
notes: raw.notes,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
375
403
|
return {
|
|
376
404
|
generatedAt: Date.now(),
|
|
377
405
|
proxy: {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cline effective-status probe.
|
|
3
|
+
* Reads providers.json and checks for opencodex / openai-compatible configuration.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { clineConfigPath, clineHomeDir } from "../config-export";
|
|
7
|
+
|
|
8
|
+
export interface ClineProbeResult {
|
|
9
|
+
present: boolean;
|
|
10
|
+
baseUrl: string | null;
|
|
11
|
+
model: string | null;
|
|
12
|
+
configPaths: string[];
|
|
13
|
+
notes: string[];
|
|
14
|
+
binary: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function resolveBinary(name: string): string | null {
|
|
18
|
+
try {
|
|
19
|
+
const which = (globalThis as { Bun?: { which?: (cmd: string) => string | null } }).Bun?.which;
|
|
20
|
+
if (typeof which === "function") {
|
|
21
|
+
return which(name) ?? null;
|
|
22
|
+
}
|
|
23
|
+
} catch { /* ignore */ }
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function probeCline(opts: { home: string; env?: NodeJS.ProcessEnv } = { home: "" }): ClineProbeResult {
|
|
28
|
+
const notes: string[] = [];
|
|
29
|
+
const binary = resolveBinary("cline");
|
|
30
|
+
const configPath = clineConfigPath(opts.env, opts.home);
|
|
31
|
+
const homeDir = clineHomeDir(opts.env, opts.home);
|
|
32
|
+
const exists = existsSync(configPath);
|
|
33
|
+
const dirExists = existsSync(homeDir);
|
|
34
|
+
const present = exists || dirExists || Boolean(binary);
|
|
35
|
+
|
|
36
|
+
if (!exists) {
|
|
37
|
+
if (present) {
|
|
38
|
+
notes.push("Cline installation detected, but providers.json is not configured yet.");
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
present,
|
|
42
|
+
baseUrl: null,
|
|
43
|
+
model: null,
|
|
44
|
+
configPaths: existsSync(homeDir) ? [homeDir] : [configPath],
|
|
45
|
+
notes,
|
|
46
|
+
binary,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let baseUrl: string | null = null;
|
|
51
|
+
let model: string | null = null;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const raw = readFileSync(configPath, "utf8");
|
|
55
|
+
const parsed = JSON.parse(raw) as {
|
|
56
|
+
lastUsedProvider?: string;
|
|
57
|
+
providers?: Record<string, { settings?: { baseUrl?: string; model?: string; provider?: string } }>;
|
|
58
|
+
};
|
|
59
|
+
const providers = parsed?.providers ?? {};
|
|
60
|
+
const ocxProvider = providers.opencodex ?? providers["openai-compatible"];
|
|
61
|
+
if (ocxProvider?.settings?.baseUrl) {
|
|
62
|
+
baseUrl = ocxProvider.settings.baseUrl;
|
|
63
|
+
model = ocxProvider.settings.model ?? null;
|
|
64
|
+
} else if (parsed?.lastUsedProvider && providers[parsed.lastUsedProvider]?.settings?.baseUrl) {
|
|
65
|
+
const active = providers[parsed.lastUsedProvider]!;
|
|
66
|
+
baseUrl = active.settings?.baseUrl ?? null;
|
|
67
|
+
model = active.settings?.model ?? null;
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
notes.push(`Could not read providers.json: ${error instanceof Error ? error.message : String(error)}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
present: true,
|
|
75
|
+
baseUrl,
|
|
76
|
+
model,
|
|
77
|
+
configPaths: [configPath],
|
|
78
|
+
notes,
|
|
79
|
+
binary,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -5,6 +5,27 @@ import { MAIN_CODEX_ACCOUNT_ID } from "./main-account";
|
|
|
5
5
|
|
|
6
6
|
export const CODEX_ACCOUNT_LOG_LABEL_RE = /^p[a-f0-9]{6}$/;
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Account log labels come in two families (#2699):
|
|
10
|
+
*
|
|
11
|
+
* - `p<hex6>` (plus the literal `main`) — a Codex pool account.
|
|
12
|
+
* - `o<hex6>` — a non-Codex OAuth provider account (xai, cursor, and siblings).
|
|
13
|
+
*
|
|
14
|
+
* Both are sha256-derived digests, never an email and never a raw provider account id. That is
|
|
15
|
+
* a privacy requirement, not a formatting preference: these labels are written to the usage log
|
|
16
|
+
* and served over the management API.
|
|
17
|
+
*
|
|
18
|
+
* hex6 is 16.7M values, so two accounts CAN collide and merge into one reported row. That is a
|
|
19
|
+
* reporting inaccuracy at operator scale, not a correctness or privacy failure, and it is the
|
|
20
|
+
* accepted cost of keeping the existing `p` format byte-compatible.
|
|
21
|
+
*/
|
|
22
|
+
export const OAUTH_ACCOUNT_LOG_LABEL_RE = /^o[a-f0-9]{6}$/;
|
|
23
|
+
export const ACCOUNT_LOG_LABEL_RE = /^(?:main|[po][a-f0-9]{6})$/;
|
|
24
|
+
|
|
25
|
+
export function oauthAccountLogLabel(accountId: string, provider = ""): string {
|
|
26
|
+
return `o${createHash("sha256").update(`${provider}\0${accountId}`).digest("hex").slice(0, 6)}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
8
29
|
export function createCodexAccountLogLabel(existingLabels: Iterable<string | undefined | null> = []): string {
|
|
9
30
|
const used = new Set([...existingLabels].filter((value): value is string => !!value));
|
|
10
31
|
for (let i = 0; i < 16; i++) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OcxConfig } from "../types";
|
|
2
|
+
import { deleteConfigTopLevelKey } from "../config/rebase-provenance";
|
|
2
3
|
|
|
3
4
|
/** Whether an account is administratively excluded from future pool selection. */
|
|
4
5
|
export function isCodexAccountPaused(config: OcxConfig, accountId: string): boolean {
|
|
@@ -12,7 +13,7 @@ export function setCodexAccountPaused(config: OcxConfig, accountId: string, paus
|
|
|
12
13
|
else pausedIds.delete(accountId);
|
|
13
14
|
|
|
14
15
|
if (pausedIds.size > 0) config.pausedCodexAccountIds = [...pausedIds];
|
|
15
|
-
else
|
|
16
|
+
else deleteConfigTopLevelKey(config, "pausedCodexAccountIds");
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export function forgetCodexAccountPause(config: OcxConfig, accountId: string): void {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isValidCodexAccountId, MAIN_CODEX_ACCOUNT_ID } from "./account-id";
|
|
2
2
|
import { DEFAULT_ACCOUNT_PRIORITY, normalizeAccountPriority } from "./pool-rotation";
|
|
3
3
|
import type { OcxConfig } from "../types";
|
|
4
|
+
import { deleteConfigTopLevelKey } from "../config/rebase-provenance";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Which ids may carry a selection order: any pool account, plus the synthetic
|
|
@@ -34,7 +35,7 @@ export function setCodexAccountPriority(config: OcxConfig, accountId: string, pr
|
|
|
34
35
|
else entries.set(accountId, priority);
|
|
35
36
|
|
|
36
37
|
if (entries.size > 0) config.codexAccountPriorities = Object.fromEntries(entries);
|
|
37
|
-
else
|
|
38
|
+
else deleteConfigTopLevelKey(config, "codexAccountPriorities");
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
export function forgetCodexAccountPriority(config: OcxConfig, accountId: string): void {
|
|
@@ -78,6 +79,6 @@ export function setCodexAccountPin(config: OcxConfig, accountId: string): void {
|
|
|
78
79
|
/** Release the pin. With `accountId` given, only when it is the pinned account. */
|
|
79
80
|
export function clearCodexAccountPin(config: OcxConfig, accountId?: string): void {
|
|
80
81
|
if (accountId === undefined || config.activeCodexAccountPinned === accountId) {
|
|
81
|
-
|
|
82
|
+
deleteConfigTopLevelKey(config, "activeCodexAccountPinned");
|
|
82
83
|
}
|
|
83
84
|
}
|
|
@@ -17,7 +17,8 @@ import {
|
|
|
17
17
|
import { readCodexCatalogPath } from "./catalog/parsing";
|
|
18
18
|
|
|
19
19
|
export const STALE_CODEX_APP_SERVER_HINT =
|
|
20
|
-
"If Codex still shows an older model list, restart its long-lived app-server process after sync (ocx sync --restart-codex)."
|
|
20
|
+
"If Codex still shows an older model list, restart its long-lived app-server process after sync (ocx sync --restart-codex). "
|
|
21
|
+
+ "On Windows the desktop app itself may also need a full restart (ocx sync --restart-desktop-app).";
|
|
21
22
|
|
|
22
23
|
/** Attach the shared dashboard hint only after a catalog or models_cache write. */
|
|
23
24
|
export function attachStaleAppServerHint<T extends {
|
|
@@ -259,6 +260,23 @@ export function isCodexAppServerCommandLine(commandLine: string, executable?: st
|
|
|
259
260
|
}
|
|
260
261
|
if (tokens.length === 0) return false;
|
|
261
262
|
if (isCodeModeHostProcess(tokens)) return true;
|
|
263
|
+
// An npm-installed Codex runs as a PAIR: `node /usr/local/bin/codex app-server` and the
|
|
264
|
+
// vendored native binary that wrapper spawns. Only the child used to match, so
|
|
265
|
+
// `--restart-codex` signalled the child while its supervisor kept holding the socket -
|
|
266
|
+
// which is what "PID(s) still running after SIGTERM" was reporting on Linux.
|
|
267
|
+
//
|
|
268
|
+
// The codex-shaped token must be IMMEDIATELY next. Skipping interpreter flags to reach
|
|
269
|
+
// it looks tempting and is wrong: interpreter options take values, so a generic skip
|
|
270
|
+
// reads the value of `node --require codex app-server worker.js` as the entrypoint.
|
|
271
|
+
// Supporting flags needs a real Node/Bun/Deno entrypoint parser, not a loop over
|
|
272
|
+
// hyphens; the observed wrappers put the path first, so this stays narrow on purpose.
|
|
273
|
+
//
|
|
274
|
+
// Dropping only the interpreter and re-running the ordinary scan is what preserves the
|
|
275
|
+
// subcommand discipline below: `node <codex> exec 'hi'` stays unmatched exactly like
|
|
276
|
+
// `codex exec 'hi'` does.
|
|
277
|
+
if (isInterpreterToken(tokens[0]!) && tokens.length > 1 && isCodexExecutableToken(tokens[1]!)) {
|
|
278
|
+
tokens = tokens.slice(1);
|
|
279
|
+
}
|
|
262
280
|
if (!isCodexExecutableToken(tokens[0]!)) return false;
|
|
263
281
|
|
|
264
282
|
let i = 1;
|
|
@@ -505,6 +523,7 @@ export function formatStaleCodexAppServerWarning(
|
|
|
505
523
|
`WARNING: ${processes.length} Codex app-server process(es) still running (PID${processes.length === 1 ? "" : "s"}: ${pids}). `
|
|
506
524
|
+ "Disk catalog/cache were updated, but Codex may keep showing the old model list until those processes restart. "
|
|
507
525
|
+ "Re-run with `ocx sync --restart-codex` (or `ocx sync-cache --restart-codex`) to send SIGTERM only to matching app-server processes. "
|
|
526
|
+
+ "On Windows the desktop app itself may also need a full restart (`ocx sync --restart-desktop-app`). "
|
|
508
527
|
+ "Active turns may be interrupted."
|
|
509
528
|
);
|
|
510
529
|
}
|
|
@@ -738,6 +757,28 @@ const CATALOG_STATE_TTL_MS = 5_000;
|
|
|
738
757
|
*/
|
|
739
758
|
const CATALOG_STATE_UNKNOWN_TTL_MS = 250;
|
|
740
759
|
|
|
760
|
+
/**
|
|
761
|
+
* How long a real observation may still be SERVED after it expires, while a refresh
|
|
762
|
+
* runs behind it (#2499).
|
|
763
|
+
*
|
|
764
|
+
* The probe is advisory and, on Windows, slow: `Invoke-CimMethod GetOwner` costs
|
|
765
|
+
* ~0.4s per candidate process, so a cold probe routinely outlives the 5s TTL and
|
|
766
|
+
* every turn that misses the cache pays for it on the request path. Serving the
|
|
767
|
+
* previous reading immediately keeps that cost off the turn without pretending it is
|
|
768
|
+
* fresh -- the refresh it triggers is what makes the next reading current.
|
|
769
|
+
*
|
|
770
|
+
* Measured from expiry rather than from when the reading was taken, so a `fresh`
|
|
771
|
+
* entry stays servable for its own TTL plus this bound. Anchoring it to expiry keeps
|
|
772
|
+
* the stale window independent of the TTL -- a cap on total age would quietly turn
|
|
773
|
+
* this path off if the TTL were ever raised past it.
|
|
774
|
+
*
|
|
775
|
+
* Bounded rather than unlimited: if the refresh keeps failing, an observation this
|
|
776
|
+
* old stops being evidence about the machine and it is better to wait for a real one.
|
|
777
|
+
* `unknown` is never served this way -- it is a failure to observe, not an
|
|
778
|
+
* observation, and it already has its own short window for exactly that reason.
|
|
779
|
+
*/
|
|
780
|
+
export const CATALOG_STATE_MAX_STALE_MS = 60_000;
|
|
781
|
+
|
|
741
782
|
export function catalogStateTtlMs(state: CodexAppServerCatalogState): number {
|
|
742
783
|
return state === "unknown" ? CATALOG_STATE_UNKNOWN_TTL_MS : CATALOG_STATE_TTL_MS;
|
|
743
784
|
}
|
|
@@ -833,6 +874,14 @@ export function collectCodexAppServerCatalogState(
|
|
|
833
874
|
* - 선택한 방식: retain the synchronous API and use async PowerShell plus an identity-scoped in-flight refresh, short cache, and invalidation generation only for Windows requests.
|
|
834
875
|
* - 다른 대안 대신 이 방식을 선택한 이유: it fixes unrelated `/healthz` starvation without widening the process-matching or restart contract.
|
|
835
876
|
* - 장점, 단점 및 영향: concurrent turns share one CIM walk, invalidated pre-write results cannot repopulate the cache, and the event loop stays responsive; a cold v2 turn can still await the bounded advisory probe.
|
|
877
|
+
*
|
|
878
|
+
* [Decision Log · #2499]
|
|
879
|
+
* - 목적과 의도: a cold probe outlives its own 5s TTL on Windows (~435ms per candidate process for `Invoke-CimMethod GetOwner`), so the cache expires before it can serve and the miss lands on a turn.
|
|
880
|
+
* - 기존 구현 및 제약 조건: the reading is advisory, and only `fresh` authorizes positive guidance (`src/server/responses/collaboration.ts`); `unknown` is a failure to observe rather than an observation.
|
|
881
|
+
* - 검토한 주요 대안: drop the per-process GetOwner fan-out (issue suggestion 1), or widen the TTL past the probe duration (suggestion 3).
|
|
882
|
+
* - 선택한 방식: serve an expired reading immediately when its generation still matches, bounded by `CATALOG_STATE_MAX_STALE_MS`, never for `unknown`, and refresh behind it; a failed refresh no longer evicts the reading it was refreshing.
|
|
883
|
+
* - 다른 대안 대신 이 방식을 선택한 이유: the fan-out change alters what "could not verify the owner" means for the current-user scoping contract and needs its own ground-truth comparison; a wider TTL still pays the probe on every human-paced turn.
|
|
884
|
+
* - 장점, 단점 및 영향: after the first probe the request path never waits; a server that stopped between readings can be described as `fresh` for up to the stale bound; a catalog write still invalidates immediately through the generation, and the cold path is unchanged.
|
|
836
885
|
*/
|
|
837
886
|
export async function collectCodexAppServerCatalogStateForRequest(
|
|
838
887
|
io: CodexAppServerProcessIo = {},
|
|
@@ -851,16 +900,30 @@ export async function collectCodexAppServerCatalogStateForRequest(
|
|
|
851
900
|
catalogMtimeMs: io.catalogMtimeMs,
|
|
852
901
|
now: io.now,
|
|
853
902
|
};
|
|
854
|
-
|
|
903
|
+
const cached = requestCatalogStateCache
|
|
855
904
|
&& requestCatalogStateCache.generation === generation
|
|
856
905
|
&& sameRequestCatalogStateIdentity(requestCatalogStateCache.identity, identity)
|
|
857
|
-
|
|
858
|
-
|
|
906
|
+
? requestCatalogStateCache
|
|
907
|
+
: null;
|
|
908
|
+
if (cached && now - cached.atMs < catalogStateTtlMs(cached.status.state)) {
|
|
909
|
+
return cached.status;
|
|
859
910
|
}
|
|
911
|
+
// An expired real reading is still worth handing back while the refresh runs. It
|
|
912
|
+
// cannot have been invalidated by an ocx catalog write: every such write calls
|
|
913
|
+
// `resetCodexAppServerCatalogStateCache`, which advances the generation and drops
|
|
914
|
+
// this entry, so a generation match means no write has landed since it was taken.
|
|
915
|
+
// What it can miss is an app-server that started or stopped meanwhile -- and a
|
|
916
|
+
// server started after the reading is newer than the catalog, which is the `fresh`
|
|
917
|
+
// this entry already says.
|
|
918
|
+
const servableStale = cached
|
|
919
|
+
&& cached.status.state !== "unknown"
|
|
920
|
+
&& now - cached.atMs < catalogStateTtlMs(cached.status.state) + CATALOG_STATE_MAX_STALE_MS
|
|
921
|
+
? cached.status
|
|
922
|
+
: null;
|
|
860
923
|
if (requestCatalogStateFlight
|
|
861
924
|
&& requestCatalogStateFlight.generation === generation
|
|
862
925
|
&& sameRequestCatalogStateIdentity(requestCatalogStateFlight.identity, identity)) {
|
|
863
|
-
return requestCatalogStateFlight.promise;
|
|
926
|
+
return servableStale ?? requestCatalogStateFlight.promise;
|
|
864
927
|
}
|
|
865
928
|
|
|
866
929
|
const refresh = async (): Promise<CodexAppServerCatalogStatus> => {
|
|
@@ -918,7 +981,18 @@ export async function collectCodexAppServerCatalogStateForRequest(
|
|
|
918
981
|
if (requestCatalogStateGeneration !== generation) {
|
|
919
982
|
return { state: "unknown" as const, processes: [], catalogMtimeMs: null };
|
|
920
983
|
}
|
|
921
|
-
|
|
984
|
+
// A refresh that failed must not evict a real reading. Before this function
|
|
985
|
+
// served stale entries, caching `unknown` cost at most the 250ms that state
|
|
986
|
+
// is allowed to live. Now that an expired observation is what callers are
|
|
987
|
+
// handed, overwriting one with `unknown` would take the answer AWAY from
|
|
988
|
+
// them on a transient failure -- `unknown` is not servable, so the next
|
|
989
|
+
// caller waits for a probe instead of getting the reading it would have had.
|
|
990
|
+
// Keep the observation and let its own age retire it.
|
|
991
|
+
const wouldEvictAnObservation = status.state === "unknown"
|
|
992
|
+
&& requestCatalogStateCache?.generation === generation
|
|
993
|
+
&& requestCatalogStateCache.status.state !== "unknown"
|
|
994
|
+
&& sameRequestCatalogStateIdentity(requestCatalogStateCache.identity, identity);
|
|
995
|
+
if (requestCatalogStateFlight === flight && !wouldEvictAnObservation) {
|
|
922
996
|
requestCatalogStateCache = {
|
|
923
997
|
generation,
|
|
924
998
|
identity,
|
|
@@ -932,7 +1006,9 @@ export async function collectCodexAppServerCatalogStateForRequest(
|
|
|
932
1006
|
});
|
|
933
1007
|
flight = { generation, identity, promise };
|
|
934
1008
|
requestCatalogStateFlight = flight;
|
|
935
|
-
|
|
1009
|
+
// `promise` already absorbs its own failures, so leaving it unawaited here cannot
|
|
1010
|
+
// surface as an unhandled rejection; the next caller picks up whatever it stored.
|
|
1011
|
+
return servableStale ?? flight.promise;
|
|
936
1012
|
}
|
|
937
1013
|
|
|
938
1014
|
/**
|
package/src/codex/auth-api.ts
CHANGED
|
@@ -209,21 +209,59 @@ function codexAccountPersistenceConflict(
|
|
|
209
209
|
: undefined;
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
/**
|
|
213
|
+
* The exact label `parseUsageQuota` emits for the Codex Spark window (quota.ts).
|
|
214
|
+
* Matching on the label rather than on "is a custom window" is load-bearing: the same array
|
|
215
|
+
* carries Cursor's First-party models / API usage, Anthropic's Fable / Opus / Sonnet,
|
|
216
|
+
* Antigravity's Gem / Cla, Kimi's subscription credits and a dozen dynamic provider meters.
|
|
217
|
+
*/
|
|
218
|
+
const CODEX_SPARK_WINDOW_LABEL = "GPT-5.3-Codex-Spark Weekly";
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Drop the Spark window unless the operator asked for it (default hidden).
|
|
222
|
+
*
|
|
223
|
+
* Applied at the DTO boundary, never at parse or cache time: custom windows participate in
|
|
224
|
+
* quota-presence checks, snapshot reconciliation and capacity aggregation, so removing Spark
|
|
225
|
+
* upstream of this point would change routing state rather than display.
|
|
226
|
+
*
|
|
227
|
+
* Both GUI surfaces funnel through here — the Codex Auth rows directly, and /api/provider-quotas
|
|
228
|
+
* via listCodexAuthAccountsSnapshot — so one filter covers both. Filtering only one would leave
|
|
229
|
+
* the other still rendering the row the operator switched off.
|
|
230
|
+
*/
|
|
231
|
+
export function withSparkVisibility<T extends Omit<StoredAccountQuota, "updatedAt"> | StoredAccountQuota | null>(
|
|
232
|
+
quota: T,
|
|
233
|
+
): T {
|
|
234
|
+
if (!quota?.customWindows?.length) return quota;
|
|
235
|
+
if (loadConfig().showCodexSparkQuota === true) return quota;
|
|
236
|
+
const kept = quota.customWindows.filter(window => window.label !== CODEX_SPARK_WINDOW_LABEL);
|
|
237
|
+
if (kept.length === quota.customWindows.length) return quota;
|
|
238
|
+
// An empty list is dropped rather than serialized: an absent field and an empty array should
|
|
239
|
+
// not be two different ways of saying "no custom windows" on the wire.
|
|
240
|
+
const next = { ...quota } as Record<string, unknown>;
|
|
241
|
+
if (kept.length > 0) next.customWindows = kept;
|
|
242
|
+
else delete next.customWindows;
|
|
243
|
+
return next as T;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
212
247
|
function quotaForPlan<T extends Omit<StoredAccountQuota, "updatedAt"> | StoredAccountQuota | null>(
|
|
213
248
|
quota: T,
|
|
214
249
|
plan: unknown,
|
|
215
250
|
): T {
|
|
216
|
-
|
|
251
|
+
const visible = withSparkVisibility(quota);
|
|
252
|
+
if (!visible || !isThirtyDayOnlyCodexPlan(plan)) return visible;
|
|
253
|
+
const quotaWindows = visible;
|
|
217
254
|
return {
|
|
218
|
-
...(
|
|
219
|
-
...(
|
|
255
|
+
...(quotaWindows.monthlyPercent !== undefined ? { monthlyPercent: quotaWindows.monthlyPercent } : {}),
|
|
256
|
+
...(quotaWindows.monthlyResetAt !== undefined ? { monthlyResetAt: quotaWindows.monthlyResetAt } : {}),
|
|
220
257
|
// A 30-day plan can still carry a burst window, and it blocks the account on its own.
|
|
221
258
|
// Dropping it here would show a healthy card for an account upstream is refusing (#1791).
|
|
222
|
-
...(
|
|
223
|
-
...(
|
|
224
|
-
...(
|
|
225
|
-
...(
|
|
226
|
-
...(
|
|
259
|
+
...(quotaWindows.shortPercent !== undefined ? { shortPercent: quotaWindows.shortPercent } : {}),
|
|
260
|
+
...(quotaWindows.shortResetAt !== undefined ? { shortResetAt: quotaWindows.shortResetAt } : {}),
|
|
261
|
+
...(quotaWindows.shortWindowSeconds !== undefined ? { shortWindowSeconds: quotaWindows.shortWindowSeconds } : {}),
|
|
262
|
+
...(quotaWindows.customWindows !== undefined ? { customWindows: quotaWindows.customWindows } : {}),
|
|
263
|
+
...(quotaWindows.resetCredits !== undefined ? { resetCredits: quotaWindows.resetCredits } : {}),
|
|
264
|
+
...("updatedAt" in quotaWindows ? { updatedAt: quotaWindows.updatedAt } : {}),
|
|
227
265
|
} as T;
|
|
228
266
|
}
|
|
229
267
|
|
|
@@ -1806,7 +1844,7 @@ export async function handleCodexAuthAPI(
|
|
|
1806
1844
|
}
|
|
1807
1845
|
|
|
1808
1846
|
if (url.pathname === "/api/codex-auth/login" && req.method === "POST") {
|
|
1809
|
-
const body = (await req.json().catch(() => ({}))) as { id?: string; reauth?: boolean };
|
|
1847
|
+
const body = (await req.json().catch(() => ({}))) as { id?: string; reauth?: boolean; openBrowser?: unknown };
|
|
1810
1848
|
const requestedAccountId = body.id?.trim();
|
|
1811
1849
|
const reauth = body.reauth === true;
|
|
1812
1850
|
if (requestedAccountId && !isValidCodexAccountId(requestedAccountId)) {
|
|
@@ -1840,7 +1878,9 @@ export async function handleCodexAuthAPI(
|
|
|
1840
1878
|
|
|
1841
1879
|
// Open the browser server-side (same pattern as /api/oauth/login in management-api.ts).
|
|
1842
1880
|
// The GUI's window.open is popup-blocked because it runs after an await, not a direct click.
|
|
1843
|
-
|
|
1881
|
+
// Both login routes share one resolver so this surface cannot drift from the other.
|
|
1882
|
+
const { shouldOpenBrowserForLogin } = await import("../oauth/open-browser-choice");
|
|
1883
|
+
if (result.url && shouldOpenBrowserForLogin(body.openBrowser, runtimeConfig)) {
|
|
1844
1884
|
const { openUrl } = await import("../lib/open-url");
|
|
1845
1885
|
openUrl(result.url);
|
|
1846
1886
|
}
|