@bitkyc08/opencodex 2.52.0-preview.20260911 → 2.52.0-preview.20260912
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/index-D_t6sCWs.js +115 -0
- package/gui/dist/assets/index-EdoPnm9_.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/devin.svg +49 -0
- package/gui/dist/provider-icons/omo.svg +42 -0
- package/package.json +3 -1
- package/src/AGENTS.md +1 -1
- package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +0 -1
- package/src/adapters/command-code.ts +0 -1
- package/src/adapters/cursor/checkpoint-store.ts +37 -0
- package/src/adapters/cursor/live-transport.ts +74 -2
- package/src/adapters/cursor.ts +6 -0
- package/src/adapters/devin/cloud-direct/auth.ts +264 -0
- package/src/adapters/devin/cloud-direct/catalog.ts +306 -0
- package/src/adapters/devin/cloud-direct/chat.ts +1274 -0
- package/src/adapters/devin/cloud-direct/index.ts +65 -0
- package/src/adapters/devin/cloud-direct/metadata.ts +134 -0
- package/src/adapters/devin/cloud-direct/wire.ts +206 -0
- package/src/adapters/devin/live-models.ts +133 -0
- package/src/adapters/devin-cli/acp.ts +204 -0
- package/src/adapters/devin-cli/adapter.ts +345 -0
- package/src/adapters/devin-cli/binary.ts +69 -0
- package/src/adapters/devin-cli/models.ts +57 -0
- package/src/adapters/devin.ts +326 -0
- package/src/adapters/google.ts +1 -1
- package/src/adapters/openai-chat.ts +31 -10
- package/src/adapters/openai-responses.ts +16 -1
- package/src/adapters/registry.ts +25 -1
- package/src/bridge.ts +8 -2
- package/src/claude/inbound-cache-stabilize.ts +130 -0
- package/src/claude/inbound.ts +45 -5
- package/src/cli/account-auth.ts +60 -1
- package/src/cli/account-extended.ts +23 -30
- package/src/cli/account.ts +2 -1
- package/src/cli/capabilities.ts +50 -8
- package/src/cli/config-command.ts +2 -2
- package/src/cli/dispatch.ts +14 -2
- package/src/cli/export-command.ts +11 -25
- package/src/cli/help.ts +2 -2
- package/src/cli/opencode.ts +5 -0
- package/src/cli/registry.ts +15 -4
- package/src/clients/config-export/cline.ts +71 -0
- package/src/clients/config-export/contracts.ts +10 -1
- package/src/clients/config-export/model-metadata.ts +33 -0
- package/src/clients/config-export/zcode.ts +23 -12
- package/src/clients/config-export.ts +156 -4
- package/src/codex/account-store.ts +7 -2
- package/src/codex/auth-context.ts +1 -1
- package/src/codex/catalog/effort.ts +4 -3
- package/src/codex/catalog/metadata.ts +8 -4
- package/src/codex/catalog/native-models.ts +24 -4
- package/src/codex/catalog/parsing.ts +19 -1
- package/src/codex/catalog/provider-fetch.ts +57 -0
- package/src/codex/catalog/sync.ts +1 -1
- package/src/codex/context-compat.ts +97 -0
- package/src/codex/context-owner.ts +201 -0
- package/src/codex/history-provider.ts +161 -14
- package/src/codex/inject-coordination.ts +3 -2
- package/src/codex/inject.ts +128 -9
- package/src/codex/pool-rotation.ts +8 -292
- package/src/codex/quota.ts +41 -12
- package/src/codex/retired-model-migration.ts +41 -0
- package/src/codex/routing.ts +327 -22
- package/src/codex/warmup.ts +2 -2
- package/src/combos/failover.ts +5 -0
- package/src/combos/resolve.ts +11 -15
- package/src/config.ts +46 -14
- package/src/generated/compatibility-version.json +293 -113
- package/src/grok/grpc-web.ts +120 -0
- package/src/grok/reset-coupon-ledger.ts +139 -0
- package/src/grok/reset-coupons.ts +278 -0
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/cline-document.ts +73 -0
- package/src/integrations/cline-io.ts +149 -0
- package/src/integrations/cline-transaction.ts +42 -0
- package/src/integrations/config-io.ts +21 -2
- package/src/integrations/journal.ts +43 -1
- package/src/integrations/omp-yaml-source.ts +123 -4
- package/src/integrations/ownership-policy.ts +7 -0
- package/src/integrations/ownership.ts +36 -0
- package/src/integrations/registry.ts +27 -0
- package/src/integrations/state.ts +5 -2
- package/src/integrations/store.ts +6 -0
- package/src/integrations/writer.ts +53 -9
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/abort.ts +36 -0
- package/src/lib/local-destinations.ts +1 -1
- package/src/lib/upstream-retry.ts +36 -1
- package/src/oauth/account-quota-rank.ts +11 -0
- package/src/oauth/callback-server.ts +10 -5
- package/src/oauth/devin/api-base.ts +63 -0
- package/src/oauth/devin/login.ts +1 -0
- package/src/oauth/devin/register-user.ts +186 -0
- package/src/oauth/devin/types.ts +71 -0
- package/src/oauth/devin-cli.ts +149 -0
- package/src/oauth/devin.ts +170 -0
- package/src/oauth/generic-account-failover.ts +169 -1
- package/src/oauth/index.ts +20 -1
- package/src/oauth/login-cli.ts +19 -5
- package/src/oauth/pool-kernel.ts +321 -0
- package/src/oauth/pool-settings-capability.ts +127 -9
- package/src/oauth/store.ts +7 -3
- package/src/oauth/token-guardian.ts +1 -1
- package/src/providers/codebuddy-models.ts +0 -3
- package/src/providers/command-code-efforts.ts +23 -4
- package/src/providers/default-aliases.ts +4 -0
- package/src/providers/derive.ts +8 -0
- package/src/providers/devin-cli-authmode-migration.ts +57 -0
- package/src/providers/key-failover.ts +175 -0
- package/src/providers/model-rename-startup.ts +21 -1
- package/src/providers/qoder-models.ts +0 -1
- package/src/providers/quota-key-accounts.ts +50 -0
- package/src/providers/quota-routing-cache.ts +65 -6
- package/src/providers/quota-types.ts +7 -0
- package/src/providers/quota.ts +113 -30
- package/src/providers/registry.ts +237 -77
- package/src/providers/stale-context-window-migration.ts +92 -0
- package/src/providers/zai-responses-migration.ts +45 -0
- package/src/quota/reset-observer.ts +2 -1
- package/src/quota/reset-seen-store.ts +9 -1
- package/src/remote-control/crypto.ts +442 -0
- package/src/remote-control/host.ts +175 -0
- package/src/remote-control/index.ts +100 -0
- package/src/remote-control/protocol.ts +200 -0
- package/src/remote-control/relay.ts +162 -0
- package/src/remote-control/workspace-agent-protocol.ts +246 -0
- package/src/remote-control/workspace-rpc-framing.ts +128 -0
- package/src/remote-control/workspace-tools.ts +237 -0
- package/src/remote-control/workspace-utf8.ts +24 -0
- package/src/responses/custom-tool-compat.ts +23 -0
- package/src/router.ts +6 -1
- package/src/routing/compatibility/behavior.ts +3 -0
- package/src/server/adapter-resolve.ts +6 -2
- package/src/server/auth-cors.ts +71 -6
- package/src/server/chat-completions.ts +5 -3
- package/src/server/chat-native.ts +9 -0
- package/src/server/claude-messages.ts +25 -30
- package/src/server/context-history.ts +207 -0
- package/src/server/images.ts +28 -1
- package/src/server/index.ts +42 -22
- package/src/server/live.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +7 -1
- package/src/server/management/config-routes.ts +3 -3
- package/src/server/management/grok-coupon-routes.ts +287 -0
- package/src/server/management/integration-routes.ts +6 -1
- package/src/server/management/oauth-account-routes.ts +124 -7
- package/src/server/management/provider-routes.ts +77 -2
- package/src/server/management/route-registry.ts +6 -1
- package/src/server/management-api.ts +7 -0
- package/src/server/request-log.ts +4 -0
- package/src/server/responses/codex-ws-exchange.ts +90 -14
- package/src/server/responses/codex-ws-wire.ts +51 -1
- package/src/server/responses/compact.ts +8 -1
- package/src/server/responses/core.ts +188 -22
- package/src/server/responses/ws-upstream.ts +1 -1
- package/src/server/responses-undeclared-tool-guard.ts +261 -12
- package/src/server/zai-responses-startup.ts +21 -0
- package/src/types/config.ts +33 -3
- package/src/types/provider.ts +54 -4
- package/src/types/request.ts +1 -1
- package/src/types/tools.ts +36 -6
- package/src/update/job.ts +33 -11
- package/src/vision/plan.ts +40 -37
- package/src/web-search/index.ts +1 -0
- package/gui/dist/assets/index-BoBRSehJ.css +0 -1
- package/gui/dist/assets/index-Dx0xv2EA.js +0 -115
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the OAuth login flow + persisted credentials.
|
|
3
|
+
*
|
|
4
|
+
* Two distinct token shapes appear in this codebase:
|
|
5
|
+
*
|
|
6
|
+
* - `firebaseIdToken` — the short-lived JWT minted by Auth0 / Firebase Auth
|
|
7
|
+
* during browser sign-in. Lives in the OAuth callback URL fragment/query.
|
|
8
|
+
* Treated as opaque and discarded once exchanged.
|
|
9
|
+
*
|
|
10
|
+
* - `apiKey` — the long-lived credential returned by
|
|
11
|
+
* `SeatManagementService.RegisterUser`. Used inside every Cascade RPC's
|
|
12
|
+
* `Metadata.api_key` field. Format is provider-defined:
|
|
13
|
+
* * Cognition era: `devin-session-token$<JWT>`
|
|
14
|
+
* * Codeium classic: bare UUID v4
|
|
15
|
+
* * Older Windsurf: `sk-ws-01-<...>` / `cog_<...>`
|
|
16
|
+
* The plugin treats it as an opaque string — only the cloud cares about format.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export interface OAuthLoginResult {
|
|
20
|
+
/** The opaque API key used as `Metadata.api_key` in every Cascade RPC. */
|
|
21
|
+
apiKey: string;
|
|
22
|
+
/** Human-readable account name (`Satvik Kapoor`). */
|
|
23
|
+
name: string;
|
|
24
|
+
/**
|
|
25
|
+
* Cloud API server (`https://server.codeium.com`, `https://eu.windsurf.com/_route/api_server`,
|
|
26
|
+
* `https://windsurf.fedstart.com/_route/api_server`). Driven by the user's
|
|
27
|
+
* tenant — language_server needs this as `--api_server_url`.
|
|
28
|
+
*/
|
|
29
|
+
apiServerUrl: string;
|
|
30
|
+
/** Optional cleanup redirect URL returned by RegisterUser. Informational. */
|
|
31
|
+
redirectUrl?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface PersistedCredentials extends OAuthLoginResult {
|
|
35
|
+
/** ISO timestamp the credentials were minted at — purely informational. */
|
|
36
|
+
issuedAt: string;
|
|
37
|
+
/** Optional tag tracking the OAuth client id used (so a future client rotation can invalidate). */
|
|
38
|
+
oauthClientId: string;
|
|
39
|
+
/**
|
|
40
|
+
* True when these credentials were written as part of the
|
|
41
|
+
* `opencode auth login` → authorize() flow (so opencode's auth.json is the
|
|
42
|
+
* authoritative copy and `opencode auth logout windsurf` should mirror-clear
|
|
43
|
+
* this file). False / absent for credentials written by our standalone
|
|
44
|
+
* `opencode-windsurf-auth login` CLI; those survive opencode auth state
|
|
45
|
+
* changes.
|
|
46
|
+
*/
|
|
47
|
+
syncedViaOpencodeAuth?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface WindsurfRegion {
|
|
51
|
+
/** Where to send users for browser sign-in. */
|
|
52
|
+
website: string;
|
|
53
|
+
/** Where to POST RegisterUser. */
|
|
54
|
+
registerApiServerUrl: string;
|
|
55
|
+
/** Auth0 client id passed in the OAuth URL. */
|
|
56
|
+
oauthClientId: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The single tenant (free / personal) configuration. EU, FedStart, and arbitrary
|
|
61
|
+
* portal URLs override `website` + `registerApiServerUrl` at runtime when the
|
|
62
|
+
* user passes `--portal-url` to the login command.
|
|
63
|
+
*/
|
|
64
|
+
export const DEFAULT_REGION: WindsurfRegion = {
|
|
65
|
+
website: 'https://windsurf.com',
|
|
66
|
+
registerApiServerUrl: 'https://register.windsurf.com',
|
|
67
|
+
// From /Applications/Windsurf.app/.../extension.js — the public Windsurf
|
|
68
|
+
// Auth0 client. If Windsurf rotates this, sign-in will start failing until
|
|
69
|
+
// we re-extract it.
|
|
70
|
+
oauthClientId: '3GUryQ7ldAeKEuD2obYnppsnmj58eP5u',
|
|
71
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Devin CLI credential import.
|
|
3
|
+
*
|
|
4
|
+
* The installed CLI writes `credentials.toml` after `devin auth login`, and the
|
|
5
|
+
* `windsurf_api_key` in it is an ordinary `devin-session-token$<JWT>` — the same
|
|
6
|
+
* shape RegisterUser returns for `ocx login devin`, and the same one the
|
|
7
|
+
* cloud-direct client already speaks. Measured against a signed-in CLI: it mints
|
|
8
|
+
* a user_jwt, opens the full model catalog, and streams chat.
|
|
9
|
+
*
|
|
10
|
+
* So this is kiro's import-first login with the same substance: adopt a signed-in
|
|
11
|
+
* local CLI's own session rather than starting a browser flow the CLI already
|
|
12
|
+
* completed. No browser is ever opened, because there is nothing left for
|
|
13
|
+
* opencodex to authorize.
|
|
14
|
+
*
|
|
15
|
+
* The file also carries `devin_webapp_host` and `devin_api_url`, which belong to
|
|
16
|
+
* the Devin *session* product (`cog_` keys, agent VMs) rather than to model
|
|
17
|
+
* inference. Neither is read here.
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
20
|
+
import { homedir } from "node:os";
|
|
21
|
+
import { posix, win32 } from "node:path";
|
|
22
|
+
import { DEVIN_CLI_INSTALL_HINT } from "../adapters/devin-cli/binary";
|
|
23
|
+
import { identityFromApiKey } from "./devin";
|
|
24
|
+
import { resolveDevinApiBaseUrl } from "./devin/api-base";
|
|
25
|
+
import type { OAuthController, OAuthCredentials } from "./types";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Structurally the `LoginOpts` from `./index`, restated here rather than imported.
|
|
29
|
+
* `index.ts` imports this module to register the provider, so importing the type
|
|
30
|
+
* back would close a cycle for one optional field this flow does not branch on:
|
|
31
|
+
* an import has nothing to force, so `forceLogin` is a no-op for it.
|
|
32
|
+
*/
|
|
33
|
+
type DevinCliLoginOpts = { forceLogin?: boolean };
|
|
34
|
+
|
|
35
|
+
/** Absolute-path override, for a CLI installed somewhere this resolver does not model. */
|
|
36
|
+
export const DEVIN_CLI_CREDENTIALS_ENV = "OPENCODEX_DEVIN_CLI_CREDENTIALS";
|
|
37
|
+
|
|
38
|
+
export interface DevinCliLoginDeps {
|
|
39
|
+
env?: NodeJS.ProcessEnv;
|
|
40
|
+
platform?: NodeJS.Platform;
|
|
41
|
+
exists?: (path: string) => boolean;
|
|
42
|
+
read?: (path: string) => string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Where the CLI keeps its own credential.
|
|
47
|
+
*
|
|
48
|
+
* Measured on a live install: `$XDG_DATA_HOME/devin/credentials.toml`, i.e.
|
|
49
|
+
* `~/.local/share/devin/...`, which `devin auth status` prints. Note this is the
|
|
50
|
+
* DATA dir, not the config dir — an earlier draft guessed `~/.config` and was
|
|
51
|
+
* wrong. The Windows branch mirrors the CLI's own installer.
|
|
52
|
+
*/
|
|
53
|
+
export function devinCliCredentialsPath(
|
|
54
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
55
|
+
platform: NodeJS.Platform = process.platform,
|
|
56
|
+
): string {
|
|
57
|
+
const override = env[DEVIN_CLI_CREDENTIALS_ENV]?.trim();
|
|
58
|
+
// Absolute only. A relative override would resolve against whatever directory
|
|
59
|
+
// the proxy happens to be running in, which is not a location a user can mean.
|
|
60
|
+
if (override && (override.startsWith("/") || /^[A-Za-z]:[\\/]/.test(override))) return override;
|
|
61
|
+
const paths = platform === "win32" ? win32 : posix;
|
|
62
|
+
if (platform === "win32") {
|
|
63
|
+
const appData = env.APPDATA ?? paths.join(homedir(), "AppData", "Roaming");
|
|
64
|
+
return paths.join(appData, "devin", "credentials.toml");
|
|
65
|
+
}
|
|
66
|
+
const dataHome = env.XDG_DATA_HOME ?? paths.join(homedir(), ".local", "share");
|
|
67
|
+
return paths.join(dataHome, "devin", "credentials.toml");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface DevinCliCredentialFile {
|
|
71
|
+
apiKey: string;
|
|
72
|
+
apiServerUrl: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Read the two keys that matter, and nothing else.
|
|
77
|
+
*
|
|
78
|
+
* The measured file is four flat `key = "value"` lines: no tables, no comments,
|
|
79
|
+
* no single quotes. A line matcher is therefore enough and a TOML dependency is
|
|
80
|
+
* not, and the quoted form is required rather than optional — an unquoted
|
|
81
|
+
* matcher would pass its own fixtures and miss the real file.
|
|
82
|
+
*
|
|
83
|
+
* Returns undefined rather than throwing so the caller owns the one error
|
|
84
|
+
* message. Nothing here ever puts the file's contents into a thrown value.
|
|
85
|
+
*/
|
|
86
|
+
export function readDevinCliCredentialFile(deps: DevinCliLoginDeps = {}): DevinCliCredentialFile | undefined {
|
|
87
|
+
const path = devinCliCredentialsPath(deps.env, deps.platform);
|
|
88
|
+
const exists = deps.exists ?? existsSync;
|
|
89
|
+
if (!exists(path)) return undefined;
|
|
90
|
+
let raw: string;
|
|
91
|
+
try {
|
|
92
|
+
raw = (deps.read ?? ((p: string) => readFileSync(p, "utf8")))(path);
|
|
93
|
+
} catch {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
const apiKey = raw.match(/^\s*windsurf_api_key\s*=\s*"([^"]+)"/m)?.[1]?.trim();
|
|
97
|
+
const apiServerUrl = raw.match(/^\s*api_server_url\s*=\s*"([^"]+)"/m)?.[1]?.trim();
|
|
98
|
+
if (!apiKey || !apiServerUrl) return undefined;
|
|
99
|
+
return { apiKey, apiServerUrl };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** True when a signed-in CLI credential is readable. Used for status, never for auth. */
|
|
103
|
+
export function devinCliSignedIn(deps: DevinCliLoginDeps = {}): boolean {
|
|
104
|
+
return readDevinCliCredentialFile(deps) !== undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function loginDevinCli(
|
|
108
|
+
ctrl: OAuthController,
|
|
109
|
+
_opts?: DevinCliLoginOpts,
|
|
110
|
+
deps: DevinCliLoginDeps = {},
|
|
111
|
+
): Promise<OAuthCredentials> {
|
|
112
|
+
const file = readDevinCliCredentialFile(deps);
|
|
113
|
+
if (!file) {
|
|
114
|
+
// Deliberately names no path contents and no parsed value. A Connect error
|
|
115
|
+
// can echo a request, and redactSecretString does not recognise a bare JWT
|
|
116
|
+
// or a devin-session-token, which is why register-user.ts refuses to repeat
|
|
117
|
+
// error bodies; the same caution applies to anything thrown from here.
|
|
118
|
+
throw new Error(
|
|
119
|
+
`No signed-in Devin CLI session found. ${DEVIN_CLI_INSTALL_HINT} Then run \`devin auth login\` and try again.`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
// The host comes off disk and then receives the key, so it passes the same
|
|
123
|
+
// allowlist as the RegisterUser host. An unallowlisted value falls back to the
|
|
124
|
+
// default rather than becoming an exfiltration target.
|
|
125
|
+
const apiBaseUrl = resolveDevinApiBaseUrl(file.apiServerUrl);
|
|
126
|
+
ctrl.onProgress?.("Imported the signed-in Devin CLI session.");
|
|
127
|
+
return {
|
|
128
|
+
access: file.apiKey,
|
|
129
|
+
// Cognition issues a durable key and exposes no refresh endpoint. Carrying
|
|
130
|
+
// the key here rather than "" is the house pattern: an empty refresh makes
|
|
131
|
+
// detectOAuthWarning report stale_credentials from the moment of login.
|
|
132
|
+
refresh: file.apiKey,
|
|
133
|
+
expires: Number.MAX_SAFE_INTEGER,
|
|
134
|
+
source: "local-cli",
|
|
135
|
+
apiBaseUrl,
|
|
136
|
+
...identityFromApiKey(file.apiKey),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function refreshDevinCliToken(
|
|
141
|
+
_refreshToken: string,
|
|
142
|
+
_signal?: AbortSignal,
|
|
143
|
+
_credential?: OAuthCredentials,
|
|
144
|
+
): Promise<OAuthCredentials> {
|
|
145
|
+
// The CLI owns this session and Cognition has no refresh endpoint. Extending
|
|
146
|
+
// the stored expiry would make a revoked key look valid forever; throwing lets
|
|
147
|
+
// the request path mark the account needsReauth instead.
|
|
148
|
+
throw new Error("invalid_grant: the Devin CLI owns this session. Run devin auth login again.");
|
|
149
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Devin / Cognition OAuth.
|
|
3
|
+
*
|
|
4
|
+
* Login opens the Auth0 browser sign-in flow (windsurf.com/windsurf/signin
|
|
5
|
+
* with redirect_uri=show-auth-token), then exchanges the pasted Firebase ID
|
|
6
|
+
* token via Cognition's RegisterUser for a long-lived API key.
|
|
7
|
+
*/
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
import type { OAuthController, OAuthCredentials } from "./types";
|
|
10
|
+
import { DEFAULT_REGION, type WindsurfRegion } from "./devin/types";
|
|
11
|
+
import { registerUser } from "./devin/register-user";
|
|
12
|
+
import { DEVIN_DEFAULT_API_SERVER, resolveDevinApiBaseUrl, validateDevinApiBaseUrl } from "./devin/api-base";
|
|
13
|
+
import { getCredential } from "./store";
|
|
14
|
+
|
|
15
|
+
export { DEVIN_DEFAULT_API_SERVER } from "./devin/api-base";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The api-server host this account must talk to.
|
|
19
|
+
*
|
|
20
|
+
* RegisterUser hands EU and FedStart tenants a host of their own and it is kept
|
|
21
|
+
* on the credential, so the signed-in account decides the destination. The
|
|
22
|
+
* configured provider baseUrl is the fallback, and the US default is the last
|
|
23
|
+
* resort; both are re-validated because neither is trusted more than the
|
|
24
|
+
* network value.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveDevinApiServer(configuredBaseUrl?: string, providerId = "devin"): string {
|
|
27
|
+
return (
|
|
28
|
+
// Provider-scoped. `devin` and `devin-cli` share this transport and this token
|
|
29
|
+
// format but sign in to different accounts, and an EU or FedStart tenant is
|
|
30
|
+
// recorded on the credential rather than in the registry. Reading a fixed
|
|
31
|
+
// "devin" slot sent one provider's key to the other's host.
|
|
32
|
+
validateDevinApiBaseUrl(getCredential(providerId)?.apiBaseUrl) ??
|
|
33
|
+
validateDevinApiBaseUrl(configuredBaseUrl) ??
|
|
34
|
+
DEVIN_DEFAULT_API_SERVER
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function decodeJwtPayload(token: string): Record<string, unknown> | undefined {
|
|
39
|
+
const parts = token.split(".");
|
|
40
|
+
const payload = parts[1];
|
|
41
|
+
if (parts.length < 2 || !payload) return undefined;
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) as Record<string, unknown>;
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function identityFromApiKey(apiKey: string): { accountId?: string; email?: string } {
|
|
50
|
+
const jwtPart = apiKey.includes("$") ? apiKey.slice(apiKey.indexOf("$") + 1) : apiKey;
|
|
51
|
+
const payload = decodeJwtPayload(jwtPart);
|
|
52
|
+
const email = typeof payload?.email === "string" && payload.email.length > 0 ? payload.email : undefined;
|
|
53
|
+
const sub = typeof payload?.sub === "string" && payload.sub.length > 0 ? payload.sub : undefined;
|
|
54
|
+
const authUid = typeof payload?.auth_uid === "string" && payload.auth_uid.length > 0 ? payload.auth_uid : undefined;
|
|
55
|
+
return { ...(email ? { email } : {}), ...(sub || authUid ? { accountId: sub ?? authUid } : {}) };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function credentialsFromApiKey(
|
|
59
|
+
apiKey: string,
|
|
60
|
+
apiBaseUrl: string,
|
|
61
|
+
source: OAuthCredentials["source"] = "oauth",
|
|
62
|
+
): OAuthCredentials {
|
|
63
|
+
const identity = identityFromApiKey(apiKey);
|
|
64
|
+
return {
|
|
65
|
+
access: apiKey,
|
|
66
|
+
// Cognition issues a durable key and exposes no refresh endpoint. Carrying
|
|
67
|
+
// the key here rather than "" is the house pattern for durable-key
|
|
68
|
+
// providers: an empty refresh makes detectOAuthWarning report
|
|
69
|
+
// stale_credentials for every Devin account from the moment it logs in.
|
|
70
|
+
refresh: apiKey,
|
|
71
|
+
// No expiry to model. A synthetic one-year deadline only produces a
|
|
72
|
+
// refresh attempt against an endpoint that does not exist.
|
|
73
|
+
expires: Number.MAX_SAFE_INTEGER,
|
|
74
|
+
source,
|
|
75
|
+
apiBaseUrl,
|
|
76
|
+
...identity,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function buildSignInUrl(region: WindsurfRegion): string {
|
|
81
|
+
const params = new URLSearchParams({
|
|
82
|
+
response_type: "token",
|
|
83
|
+
client_id: region.oauthClientId,
|
|
84
|
+
redirect_uri: "show-auth-token",
|
|
85
|
+
state: randomUUID(),
|
|
86
|
+
prompt: "login",
|
|
87
|
+
});
|
|
88
|
+
return region.website + "/windsurf/signin?" + params.toString();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Shape of the value the sign-in page hands back.
|
|
93
|
+
*
|
|
94
|
+
* It is not always a JWT. A live free-tier sign-in against
|
|
95
|
+
* windsurf.com/windsurf/signin returns a 47-character one-time token of the
|
|
96
|
+
* form `ott$<base64url>`, and RegisterUser accepts it; an earlier JWT-only
|
|
97
|
+
* check here would have rejected every real login. So this is deliberately a
|
|
98
|
+
* shape check for "one opaque credential-looking word" rather than a format
|
|
99
|
+
* check: the point is to tell a token from a pasted URL or a sentence, not to
|
|
100
|
+
* second-guess what the vendor mints.
|
|
101
|
+
*/
|
|
102
|
+
const TOKEN_SHAPE = /^[A-Za-z0-9._$~+/=-]{20,4096}$/;
|
|
103
|
+
|
|
104
|
+
const TOKEN_PARAM_NAMES = ["firebase_id_token", "access_token", "id_token", "token"] as const;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Turn whatever the user pasted into the Firebase ID token RegisterUser expects.
|
|
108
|
+
*
|
|
109
|
+
* The sign-in page shows a bare token, but a user who copies the address bar
|
|
110
|
+
* instead hands us a callback URL whose fragment carries it. Posting that URL
|
|
111
|
+
* as `firebase_id_token` produces an opaque server-side rejection, so pull the
|
|
112
|
+
* token out and refuse a paste that has none rather than sending something that
|
|
113
|
+
* cannot work.
|
|
114
|
+
*/
|
|
115
|
+
export function parseDevinAuthPaste(raw: string): string {
|
|
116
|
+
const trimmed = raw.trim();
|
|
117
|
+
if (!trimmed) throw new Error("No auth token pasted; cannot complete Devin sign-in.");
|
|
118
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
119
|
+
let url: URL;
|
|
120
|
+
try {
|
|
121
|
+
url = new URL(trimmed);
|
|
122
|
+
} catch {
|
|
123
|
+
throw new Error("That paste is not a usable Devin auth token or sign-in URL.");
|
|
124
|
+
}
|
|
125
|
+
const hash = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
|
|
126
|
+
for (const params of [new URLSearchParams(hash), url.searchParams]) {
|
|
127
|
+
for (const name of TOKEN_PARAM_NAMES) {
|
|
128
|
+
const value = params.get(name)?.trim();
|
|
129
|
+
if (value && TOKEN_SHAPE.test(value)) return value;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
throw new Error("That sign-in URL carries no auth token. Paste the token shown on the Windsurf page instead.");
|
|
133
|
+
}
|
|
134
|
+
if (TOKEN_SHAPE.test(trimmed)) return trimmed;
|
|
135
|
+
throw new Error("That paste is not a Devin auth token. Copy the token shown on the Windsurf sign-in page.");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function loginDevinBrowser(ctrl: OAuthController, region: WindsurfRegion): Promise<OAuthCredentials> {
|
|
139
|
+
const url = buildSignInUrl(region);
|
|
140
|
+
ctrl.onAuth?.({
|
|
141
|
+
url,
|
|
142
|
+
instructions: "Sign in with your Cognition/Devin account, then paste the on-screen auth token here.",
|
|
143
|
+
});
|
|
144
|
+
ctrl.onProgress?.("Waiting for the pasted auth token...");
|
|
145
|
+
const pasted = (await ctrl.onManualCodeInput?.())?.trim();
|
|
146
|
+
if (!pasted) throw new Error("No auth token pasted; cannot complete Devin sign-in.");
|
|
147
|
+
const firebaseIdToken = parseDevinAuthPaste(pasted);
|
|
148
|
+
const result = await registerUser(firebaseIdToken, region, ctrl.signal);
|
|
149
|
+
const credentials = credentialsFromApiKey(result.apiKey, resolveDevinApiBaseUrl(result.apiServerUrl), "oauth");
|
|
150
|
+
// The display name is not an identity. Use it only when the key carried no
|
|
151
|
+
// email, otherwise reauth compares a label against an address and mismatches.
|
|
152
|
+
if (!credentials.email && result.name) credentials.email = result.name;
|
|
153
|
+
return credentials;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function loginDevin(ctrl: OAuthController): Promise<OAuthCredentials> {
|
|
157
|
+
return loginDevinBrowser(ctrl, DEFAULT_REGION);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function refreshDevinToken(
|
|
161
|
+
_refreshToken: string,
|
|
162
|
+
_signal?: AbortSignal,
|
|
163
|
+
_credential?: OAuthCredentials,
|
|
164
|
+
): Promise<OAuthCredentials> {
|
|
165
|
+
// Cognition has no refresh endpoint. Extending the stored expiry here is what
|
|
166
|
+
// the carried implementation did, and it makes a revoked key look valid
|
|
167
|
+
// forever. Throwing lets the request path mark the account needsReauth the
|
|
168
|
+
// first time a forced refresh happens.
|
|
169
|
+
throw new Error("invalid_grant: Devin API keys do not refresh. Run ocx login devin again.");
|
|
170
|
+
}
|
|
@@ -16,7 +16,21 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { getAccountSet } from "./store";
|
|
18
18
|
import { getValidAccessSnapshotForAccount, type OAuthAccessSnapshot } from "./index";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
accountHeadroomPercent,
|
|
21
|
+
exhaustedCooldownMs,
|
|
22
|
+
hasHeadroomEvidence,
|
|
23
|
+
isAccountQuotaExhausted,
|
|
24
|
+
rankAccountsByHeadroom,
|
|
25
|
+
} from "./account-quota-rank";
|
|
26
|
+
import {
|
|
27
|
+
genericPoolKey,
|
|
28
|
+
normalizeAccountPoolStickyLimit,
|
|
29
|
+
notePoolRotationSuccess,
|
|
30
|
+
peekRoundRobinAccount,
|
|
31
|
+
pickRoundRobinAccount,
|
|
32
|
+
seedPoolRotationAccount,
|
|
33
|
+
} from "./pool-kernel";
|
|
20
34
|
import { parseRetryAfterMs } from "../combos/failover";
|
|
21
35
|
import { sweepExpiredOnWrite } from "../lib/state-store-sweeper";
|
|
22
36
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
@@ -169,6 +183,113 @@ export function eligibleFailoverAccounts(providerName: string, now = Date.now())
|
|
|
169
183
|
.map(account => account.id);
|
|
170
184
|
}
|
|
171
185
|
|
|
186
|
+
/** Generic pool strategies the kernel can actually run. `quota` IS the pre-kernel path. */
|
|
187
|
+
type ActiveGenericStrategy = "round-robin" | "fill-first";
|
|
188
|
+
|
|
189
|
+
/** Matches the Codex and Anthropic pools; the DTO still reports `null` for "not stored". */
|
|
190
|
+
const DEFAULT_GENERIC_AUTO_SWITCH_THRESHOLD = 80;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The strategy this provider's pool actually runs, or null for today's behaviour.
|
|
194
|
+
*
|
|
195
|
+
* Three different inputs answer null and they all mean the same thing to a caller: the flag is
|
|
196
|
+
* off, no strategy is stored, or the stored strategy is `quota` — which is precisely what the
|
|
197
|
+
* unflagged code already does. Collapsing them here is what keeps every call site a two-way
|
|
198
|
+
* branch instead of a four-way one.
|
|
199
|
+
*/
|
|
200
|
+
function activeGenericStrategy(config: OcxConfig, providerName: string): ActiveGenericStrategy | null {
|
|
201
|
+
if (config.pool?.kernel !== true) return null;
|
|
202
|
+
const raw = config.providers?.[providerName]?.oauthAccountFailover?.strategy;
|
|
203
|
+
return raw === "round-robin" || raw === "fill-first" ? raw : null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function genericStickyLimit(config: OcxConfig, providerName: string): number {
|
|
207
|
+
return normalizeAccountPoolStickyLimit(config.providers?.[providerName]?.oauthAccountFailover?.stickyLimit);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* The FULL roster in a stable order, not the eligible subset.
|
|
212
|
+
*
|
|
213
|
+
* Two load-bearing reasons. The store holds accounts in LOGIN order, so two operators who added
|
|
214
|
+
* the same accounts in a different sequence would otherwise rotate differently; sorting makes
|
|
215
|
+
* the ring a property of the accounts rather than of the history. And walking the eligible
|
|
216
|
+
* subset instead of the full roster changes the wrap order whenever an ineligible id sits
|
|
217
|
+
* between two eligible ones — the bug the Codex and Anthropic copies carry a `stableAll`
|
|
218
|
+
* argument to avoid.
|
|
219
|
+
*/
|
|
220
|
+
function stableGenericRoster(providerName: string): string[] {
|
|
221
|
+
const set = getAccountSet(providerName);
|
|
222
|
+
if (!set) return [];
|
|
223
|
+
return set.accounts.map(account => account.id).sort((left, right) => left.localeCompare(right));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Has this account spent enough of its allowance for fill-first to move on?
|
|
228
|
+
*
|
|
229
|
+
* An unmeasured account reads as UNDER the threshold, matching the Codex pool: a threshold is a
|
|
230
|
+
* statement about observed usage, and treating "no observation" as "spent" would evacuate every
|
|
231
|
+
* quota-less provider off its active account on the very first request.
|
|
232
|
+
*/
|
|
233
|
+
function isOverAutoSwitchThreshold(providerName: string, accountId: string, threshold: number): boolean {
|
|
234
|
+
const headroom = accountHeadroomPercent(providerName, accountId);
|
|
235
|
+
if (headroom === null) return false;
|
|
236
|
+
return 100 - headroom >= threshold;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Fill-first: stay on the active account until it crosses its threshold, then take the next
|
|
241
|
+
* eligible account in the stable ring. Null means "keep the active account".
|
|
242
|
+
*/
|
|
243
|
+
function pickFillFirstGenericAccount(
|
|
244
|
+
config: OcxConfig,
|
|
245
|
+
providerName: string,
|
|
246
|
+
activeId: string | undefined,
|
|
247
|
+
now: number,
|
|
248
|
+
): string | null {
|
|
249
|
+
const stableAll = stableGenericRoster(providerName);
|
|
250
|
+
if (stableAll.length < 2) return null;
|
|
251
|
+
const eligible = new Set(eligibleFailoverAccounts(providerName, now));
|
|
252
|
+
const stored = config.providers?.[providerName]?.oauthAccountFailover?.autoSwitchThreshold;
|
|
253
|
+
const threshold = typeof stored === "number" && Number.isInteger(stored) && stored >= 0 && stored <= 100
|
|
254
|
+
? stored
|
|
255
|
+
: DEFAULT_GENERIC_AUTO_SWITCH_THRESHOLD;
|
|
256
|
+
if (activeId && eligible.has(activeId) && !isOverAutoSwitchThreshold(providerName, activeId, threshold)) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
const start = activeId ? stableAll.indexOf(activeId) : -1;
|
|
260
|
+
const ring = start >= 0 ? [...stableAll.slice(start + 1), ...stableAll.slice(0, start)] : stableAll;
|
|
261
|
+
for (const id of ring) {
|
|
262
|
+
if (id !== activeId && eligible.has(id)) return id;
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Advance the round-robin cursor once a dispatch has actually been admitted on this account.
|
|
269
|
+
*
|
|
270
|
+
* The early return is the whole safety story for the core path: this is reached on EVERY
|
|
271
|
+
* generic first dispatch, including quota pools and the fallback after a preferred account was
|
|
272
|
+
* dropped, so anything but round-robin must leave the cursor untouched.
|
|
273
|
+
*
|
|
274
|
+
* The live pick belongs here rather than in the proposal, and that is not stylistic.
|
|
275
|
+
* `peekRoundRobinAccount` never creates the pool state and `notePoolRotationSuccess` returns
|
|
276
|
+
* immediately when there is none, so a peek-only path would leave the ring with nothing to
|
|
277
|
+
* advance and round-robin would propose the same account forever. This is the same shape
|
|
278
|
+
* `commitAnthropicSelectionRouting` already commits with.
|
|
279
|
+
*/
|
|
280
|
+
export function noteGenericPoolSelection(config: OcxConfig, providerName: string, accountId: string): void {
|
|
281
|
+
if (activeGenericStrategy(config, providerName) !== "round-robin") return;
|
|
282
|
+
const poolKey = genericPoolKey(providerName);
|
|
283
|
+
const limit = genericStickyLimit(config, providerName);
|
|
284
|
+
const picked = pickRoundRobinAccount(poolKey, eligibleFailoverAccounts(providerName), limit);
|
|
285
|
+
// The resolver may have admitted a different account than the ring proposed: a removal, a
|
|
286
|
+
// reauth verdict or a manual selection can land during credential resolution. Realign the
|
|
287
|
+
// cursor onto what actually served rather than leaving it on a road not taken.
|
|
288
|
+
if (picked !== accountId) seedPoolRotationAccount(poolKey, accountId);
|
|
289
|
+
notePoolRotationSuccess(poolKey, accountId, limit);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
|
|
172
293
|
/**
|
|
173
294
|
* Cool the account that actually 429'd and name the next eligible one, or null.
|
|
174
295
|
*
|
|
@@ -212,6 +333,30 @@ export function rotateGenericOAuthAccountOn429(
|
|
|
212
333
|
const ring = start >= 0 ? [...order.slice(start + 1), ...order.slice(0, start)] : order;
|
|
213
334
|
const candidates = ring.filter(id => id !== failedAccountId && eligible.includes(id));
|
|
214
335
|
if (candidates.length === 0) return null;
|
|
336
|
+
// The 429 path branches too. Leaving it on the quota ranking would make a configured
|
|
337
|
+
// strategy inert in practice the moment anything actually failed, which is the case the
|
|
338
|
+
// operator chose the strategy for.
|
|
339
|
+
const strategy = activeGenericStrategy(config, providerName);
|
|
340
|
+
if (strategy === "round-robin") {
|
|
341
|
+
// PICK here, not peek: the failure already happened and this answer is the one being used,
|
|
342
|
+
// so the ring genuinely advances.
|
|
343
|
+
return pickRoundRobinAccount(
|
|
344
|
+
genericPoolKey(providerName),
|
|
345
|
+
candidates,
|
|
346
|
+
genericStickyLimit(config, providerName),
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
if (strategy === "fill-first") {
|
|
350
|
+
// Not "keep the active account": the one that just 429'd is cooled, so fill-first takes
|
|
351
|
+
// the next eligible account in the stable ring rather than its usual hold.
|
|
352
|
+
const stableAll = stableGenericRoster(providerName);
|
|
353
|
+
const from = stableAll.indexOf(failedAccountId);
|
|
354
|
+
const walk = from >= 0 ? [...stableAll.slice(from + 1), ...stableAll.slice(0, from)] : stableAll;
|
|
355
|
+
for (const id of walk) {
|
|
356
|
+
if (id !== failedAccountId && candidates.includes(id)) return id;
|
|
357
|
+
}
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
215
360
|
// With no quota evidence this returns the ring untouched, so providers without
|
|
216
361
|
// per-account quota keep exactly the traversal they have today.
|
|
217
362
|
return rankAccountsByHeadroom(providerName, candidates)[0] ?? null;
|
|
@@ -259,6 +404,29 @@ export function preferredInitialAccount(
|
|
|
259
404
|
const order = selected.accounts.filter(account => account.needsReauth !== true).map(account => account.id);
|
|
260
405
|
if (order.length < 2) return null;
|
|
261
406
|
|
|
407
|
+
// A configured strategy answers this question itself. Both guards below exist to protect the
|
|
408
|
+
// QUOTA answer, and both are fatal to the other two: hasHeadroomEvidence refuses every
|
|
409
|
+
// provider with no quota data, which is exactly where round-robin is the point, and the
|
|
410
|
+
// healthy-active return fires before autoSwitchThreshold can ever be read, so fill-first
|
|
411
|
+
// would never reach its own test. Cooldowns and reauth are still honoured inside each pick.
|
|
412
|
+
const strategy = activeGenericStrategy(config, providerName);
|
|
413
|
+
if (strategy === "round-robin") {
|
|
414
|
+
const eligibleNow = eligibleFailoverAccounts(providerName, now);
|
|
415
|
+
if (eligibleNow.length === 0) return null;
|
|
416
|
+
// PEEK, not pick: this proposal is discardable, and advancing the ring for an account the
|
|
417
|
+
// resolver then rejects would skip a turn for nothing. noteGenericPoolSelection commits.
|
|
418
|
+
const picked = peekRoundRobinAccount(
|
|
419
|
+
genericPoolKey(providerName),
|
|
420
|
+
eligibleNow,
|
|
421
|
+
genericStickyLimit(config, providerName),
|
|
422
|
+
);
|
|
423
|
+
return picked && picked !== active ? picked : null;
|
|
424
|
+
}
|
|
425
|
+
if (strategy === "fill-first") {
|
|
426
|
+
const picked = pickFillFirstGenericAccount(config, providerName, active, now);
|
|
427
|
+
return picked && picked !== active ? picked : null;
|
|
428
|
+
}
|
|
429
|
+
|
|
262
430
|
const activeRow = selected.accounts.find(account => account.id === active);
|
|
263
431
|
if (activeRow && activeRow.needsReauth !== true
|
|
264
432
|
&& !isCooled(providerName, activeRow.id, now)
|
package/src/oauth/index.ts
CHANGED
|
@@ -38,6 +38,8 @@ import { loginNous, NousTokenError, refreshNousToken, clearNousRefreshIntent, Re
|
|
|
38
38
|
import { loginChatGPT, refreshChatGPTToken, type ChatGPTLoginFlow } from "./chatgpt";
|
|
39
39
|
import { loginAntigravity, refreshAntigravityToken } from "./google-antigravity";
|
|
40
40
|
import { loginCursor, refreshCursorToken } from "./cursor";
|
|
41
|
+
import { loginDevin, refreshDevinToken } from "./devin";
|
|
42
|
+
import { loginDevinCli, refreshDevinCliToken } from "./devin-cli";
|
|
41
43
|
import { loginGithubCopilot, refreshGithubCopilotToken, validateCopilotApiBaseUrl } from "./github-copilot";
|
|
42
44
|
import { loginCommandCode, refreshCommandCodeToken } from "./command-code";
|
|
43
45
|
import { loginMetaMuse, refreshMetaMuseToken } from "./meta-muse";
|
|
@@ -308,6 +310,23 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderDef> = {
|
|
|
308
310
|
providerConfig: oauthConfig("cursor"),
|
|
309
311
|
defaultModel: oauthDefaultModel("cursor"),
|
|
310
312
|
},
|
|
313
|
+
devin: {
|
|
314
|
+
login: (ctrl) => loginDevin(ctrl),
|
|
315
|
+
refresh: refreshDevinToken,
|
|
316
|
+
providerConfig: oauthConfig("devin"),
|
|
317
|
+
defaultModel: oauthDefaultModel("devin"),
|
|
318
|
+
defaultRefreshPolicy: "disabled",
|
|
319
|
+
},
|
|
320
|
+
"devin-cli": {
|
|
321
|
+
// Import-first, the kiro shape: adopt the credential the installed CLI
|
|
322
|
+
// already holds instead of starting a browser flow it has already completed.
|
|
323
|
+
login: (ctrl, opts) => loginDevinCli(ctrl, opts),
|
|
324
|
+
refresh: refreshDevinCliToken,
|
|
325
|
+
providerConfig: oauthConfig("devin-cli"),
|
|
326
|
+
defaultModel: oauthDefaultModel("devin-cli"),
|
|
327
|
+
// The CLI owns the session and Cognition exposes no refresh endpoint.
|
|
328
|
+
defaultRefreshPolicy: "disabled",
|
|
329
|
+
},
|
|
311
330
|
"github-copilot": {
|
|
312
331
|
login: (ctrl) => loginGithubCopilot(ctrl),
|
|
313
332
|
refresh: (rt, signal) => refreshGithubCopilotToken(rt, signal),
|
|
@@ -320,7 +339,7 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderDef> = {
|
|
|
320
339
|
login: (ctrl, opts) => loginChatGPT(ctrl, { forceLogin: opts?.forceLogin, flow: opts?.flow }),
|
|
321
340
|
refresh: (rt) => refreshChatGPTToken(rt),
|
|
322
341
|
providerConfig: { adapter: "openai-responses", baseUrl: "https://chatgpt.com/backend-api/codex", authMode: "forward" as const },
|
|
323
|
-
defaultModel: "gpt-5.
|
|
342
|
+
defaultModel: "gpt-5.6-luna",
|
|
324
343
|
},
|
|
325
344
|
};
|
|
326
345
|
|
package/src/oauth/login-cli.ts
CHANGED
|
@@ -65,15 +65,29 @@ export function warnIfLiveReloadSkipped(result: LocalProviderReloadResult | null
|
|
|
65
65
|
);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* The provider wall is the first thing an unfamiliar user sees, so it names the Codex
|
|
70
|
+
* route before the ~90 provider ids. 'codex' is not in either list on purpose: it is
|
|
71
|
+
* routed to the account-pool login in dispatch.ts, and 'chatgpt' stays off the public
|
|
72
|
+
* OAuth surface (isPublicOAuthProvider) because the pool owns that credential.
|
|
73
|
+
*
|
|
74
|
+
* It names 'openai-apikey' for the same reason it exists at all: 'openai' now routes to
|
|
75
|
+
* the pool, so someone who typed it looking for a platform key no longer sees the list
|
|
76
|
+
* that used to be their only pointer to it.
|
|
77
|
+
*/
|
|
78
|
+
export function loginUsageMessage(): string {
|
|
79
|
+
return `Usage: ocx login <provider>\n`
|
|
80
|
+
+ ` Codex / ChatGPT: ocx login codex (account pool, needs a running proxy; 'chatgpt' and\n`
|
|
81
|
+
+ ` 'openai' are the same route. An OpenAI platform key is 'openai-apikey'.)\n`
|
|
82
|
+
+ ` OAuth login: ${listOAuthProviders().join(", ")}\n`
|
|
83
|
+
+ ` API-key login: ${Object.keys(KEY_LOGIN_PROVIDERS).join(", ")}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
68
86
|
export async function handleLogin(provider?: string): Promise<void> {
|
|
69
87
|
const name = (provider ?? "").trim().toLowerCase();
|
|
70
88
|
if (isPublicOAuthProvider(name)) return handleOAuthLogin(name);
|
|
71
89
|
if (isKeyLoginProvider(name)) return handleKeyLogin(name);
|
|
72
|
-
console.error(
|
|
73
|
-
`Usage: ocx login <provider>\n` +
|
|
74
|
-
` OAuth login: ${listOAuthProviders().join(", ")}\n` +
|
|
75
|
-
` API-key login: ${Object.keys(KEY_LOGIN_PROVIDERS).join(", ")}`,
|
|
76
|
-
);
|
|
90
|
+
console.error(loginUsageMessage());
|
|
77
91
|
process.exit(1);
|
|
78
92
|
}
|
|
79
93
|
|