egaki 0.9.0 → 0.11.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/CHANGELOG.md +72 -0
- package/dist/cli/chatgpt-auth.d.ts +30 -5
- package/dist/cli/chatgpt-auth.d.ts.map +1 -1
- package/dist/cli/chatgpt-auth.js +257 -79
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +132 -28
- package/dist/cli/dev-server.d.ts +30 -0
- package/dist/cli/dev-server.d.ts.map +1 -0
- package/dist/cli/dev-server.js +266 -0
- package/dist/cli/dev-server.test.d.ts +2 -0
- package/dist/cli/dev-server.test.d.ts.map +1 -0
- package/dist/cli/dev-server.test.js +95 -0
- package/dist/cli/generate.d.ts +8 -0
- package/dist/cli/generate.d.ts.map +1 -1
- package/dist/cli/generate.js +170 -58
- package/dist/cli/generate.test.js +50 -1
- package/dist/cli/login.d.ts +38 -3
- package/dist/cli/login.d.ts.map +1 -1
- package/dist/cli/login.js +44 -17
- package/dist/cli/login.test.d.ts +2 -0
- package/dist/cli/login.test.d.ts.map +1 -0
- package/dist/cli/login.test.js +158 -0
- package/dist/cli/open-browser.d.ts +3 -1
- package/dist/cli/open-browser.d.ts.map +1 -1
- package/dist/cli/open-browser.js +2 -2
- package/dist/cli/xai-auth.d.ts +3 -1
- package/dist/cli/xai-auth.d.ts.map +1 -1
- package/dist/cli/xai-auth.js +13 -3
- package/dist/vite/angled-screen-shader.d.ts +5 -0
- package/dist/vite/angled-screen-shader.d.ts.map +1 -1
- package/dist/vite/angled-screen-shader.js +77 -34
- package/dist/vite/generated-media-client.d.ts.map +1 -1
- package/dist/vite/generated-media-client.js +2 -2
- package/dist/vite/vite-plugin.d.ts.map +1 -1
- package/dist/vite/vite-plugin.js +128 -10
- package/package.json +6 -10
- package/skills/egaki/SKILL.md +14 -0
- package/src/cli/chatgpt-auth.ts +379 -89
- package/src/cli/cli.ts +161 -27
- package/src/cli/dev-server.test.ts +109 -0
- package/src/cli/dev-server.ts +303 -0
- package/src/cli/generate.test.ts +57 -0
- package/src/cli/generate.ts +191 -61
- package/src/cli/login.test.ts +185 -0
- package/src/cli/login.ts +68 -16
- package/src/cli/open-browser.ts +7 -6
- package/src/cli/xai-auth.ts +15 -3
- package/src/vite/angled-screen-shader.tsx +81 -34
- package/src/vite/generated-media-client.tsx +6 -1
- package/src/vite/vite-plugin.ts +137 -10
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,78 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.11.0
|
|
6
|
+
|
|
7
|
+
1. **Background OAuth login for agents and non-TTY shells.** `egaki login --provider chatgpt` and `egaki login --provider xai-oauth` start the browser login and return. An interactive terminal still stays attached until login finishes.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
egaki login --provider chatgpt
|
|
11
|
+
# Login is running in the background and the browser will open.
|
|
12
|
+
# After approval, verify with: egaki login status chatgpt
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
2. **New `egaki login status` command** — check one provider. The command exits with status 1 until that login is complete:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
egaki login status chatgpt
|
|
19
|
+
egaki login status xai-oauth
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
3. **Shell completions** — install, print, or remove the completion script:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
egaki completions install
|
|
26
|
+
egaki completions script
|
|
27
|
+
egaki completions uninstall
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
4. **`--aspect-ratio` works on OpenAI image models.** `gpt-image-*` and `dall-e-*` have no native aspect-ratio parameter. The flag now picks the closest supported size and states the ratio in the prompt. A 16:9 request on `gpt-image-1-mini` produces 1536x1024 instead of a silent 1024x1024 square:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
egaki image "a red cube on a white table" \
|
|
34
|
+
-m gpt-image-1-mini --aspect-ratio 16:9
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
5. **ChatGPT image generation uses the account model list.** A fixed slug such as `gpt-5.4` is no longer sent. Generation uses the highest-priority model that is visible, API-supported, and included in the saved plan. If that model is rejected, it retries once with the next listed model.
|
|
38
|
+
|
|
39
|
+
6. **ChatGPT login is saved only after the token exchange succeeds.** The redirect uses `localhost`, which the allow-list accepts. The listener stays on `127.0.0.1`. A failed exchange does not replace the previous login. `egaki login --show` no longer calls a revoked token valid. It says the token is not expired locally, or that the next request will try to refresh.
|
|
40
|
+
|
|
41
|
+
7. **`egaki subscribe --plan` accepts only `plus` and `pro`.** Plus is $29/mo. Pro is $99/mo. An invalid plan name fails in the CLI instead of opening a buy URL that returns 400:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
egaki subscribe --plan plus
|
|
45
|
+
egaki subscribe --plan pro
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
8. **Fixed HMR for a root `.mdx` file that is also imported.** Editing `tagline.mdx` when another file imports it updates the player without a manual reload.
|
|
49
|
+
|
|
50
|
+
9. **`egaki dev` prints SDK method names and a docs link** after the server URL, including `seekTo`, `screenshot`, `filmstrip`, `export`, and `getInfo`.
|
|
51
|
+
|
|
52
|
+
## 0.10.0
|
|
53
|
+
|
|
54
|
+
1. **New `egaki dev` command — zero-config MDX video dev server.** Serve an `.mdx` file with no project setup at all: no `package.json`, no `vite.config.ts`, no `npm install`. All dependencies (react, remotion, vite) come from the egaki CLI's own installation:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Serve a single MDX file, no project setup
|
|
58
|
+
egaki dev video.mdx
|
|
59
|
+
|
|
60
|
+
# Serve the current directory (auto-discovers video.mdx > index.mdx > first .mdx)
|
|
61
|
+
egaki dev
|
|
62
|
+
|
|
63
|
+
# Fixed port + open browser
|
|
64
|
+
egaki dev intro.mdx --port 5199 --open
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Uses a random free port by default; `--port`, `--host`, and `--open` are available. Under the hood a shim `node_modules` with symlinks to egaki's own dependencies is created in the folder, so Vite's dep optimizer, Tailwind scans, and even editor TypeScript resolution work in scratch folders. If the folder has a foreign `node_modules` without egaki, you get a clear error with guidance.
|
|
68
|
+
|
|
69
|
+
2. **SDK usage instructions printed on dev server startup** — after Vite's ready banner, the dev server prints copy-pasteable `window.egakiSDK` examples (getInfo, screenshot, filmstrip, export, seekTo via Playwriter) so agents reading terminal output immediately know how to control the running player.
|
|
70
|
+
|
|
71
|
+
3. **`<AngledScreen>` hexagonal iris bokeh + new `chromaticAberration` prop** — the depth-of-field kernel is now a sparse 6-blade hexagonal iris, so bright points form hard-edged aperture disks instead of smoothing into a gaussian. The new `chromaticAberration` prop (default `0.45`) adds a radial R/B channel split that scales with local blur, so purple/cyan fringing reads strongest in the bokeh like a real lens.
|
|
72
|
+
|
|
73
|
+
4. **Fixed "No video config found" with duplicate remotion copies** — under pnpm, when remotion was only a transitive dep of egaki (or the app installed remotion directly while `file:`-linking egaki), two physical copies split the Player context from `useVideoConfig`. All `remotion`, `@remotion/*`, and `motion` imports now converge on a single copy.
|
|
74
|
+
|
|
75
|
+
5. **Fixed browser crash `"does not provide an export named 'default'"`** on portable npm installs of egaki — nested pure-CJS packages under safe-mdx (format, fault, micromark chain) were split into named-only ESM chunks by the dep optimizer. They now stay internalized with correct CJS interop.
|
|
76
|
+
|
|
5
77
|
## 0.9.0
|
|
6
78
|
|
|
7
79
|
1. **`<AngledScreen>` with true WebGL depth-of-field** — the 3D tilted screen component is now shader-based. Children DOM is captured per frame with Chromium's HTML-in-canvas API and rendered as a perspective-tilted plane in a WebGL2 fragment shader, giving true per-pixel depth. Depth drives bokeh blur (the exact Three.js BokehShader 41-tap ring kernel), fog toward `backgroundColor`, and subtle film grain. Available in MDX without imports:
|
|
@@ -8,15 +8,40 @@ export type ChatGptAuth = {
|
|
|
8
8
|
};
|
|
9
9
|
/** Extract the ChatGPT plan type (e.g. "plus", "pro") from the access token. */
|
|
10
10
|
export declare function extractPlanType(auth: ChatGptAuth): string | undefined;
|
|
11
|
+
export declare function isRevokedChatGptAccessToken(status: number, body: string): boolean;
|
|
12
|
+
export declare function oauthCallbackAction({ pendingState, callbackState, error, code, }: {
|
|
13
|
+
pendingState: string | undefined;
|
|
14
|
+
callbackState: string | null;
|
|
15
|
+
error: string | null;
|
|
16
|
+
code: string | null;
|
|
17
|
+
}): 'ignore' | 'provider-error' | 'missing-code' | 'exchange';
|
|
18
|
+
export declare function chatGptReLoginMessage({ status, body, }: {
|
|
19
|
+
status: number;
|
|
20
|
+
body: string;
|
|
21
|
+
}): string;
|
|
22
|
+
export type ChatGptResponsesModel = {
|
|
23
|
+
slug: string;
|
|
24
|
+
visibility?: string;
|
|
25
|
+
priority?: number;
|
|
26
|
+
supportedInApi?: boolean;
|
|
27
|
+
plans?: string[];
|
|
28
|
+
};
|
|
29
|
+
export declare function pickChatGptResponsesModel({ models, plan, exclude, }: {
|
|
30
|
+
models: ChatGptResponsesModel[];
|
|
31
|
+
plan: string | undefined;
|
|
32
|
+
exclude?: string[];
|
|
33
|
+
}): string | null;
|
|
34
|
+
export declare function resolveChatGptResponsesModel({ auth, exclude, }: {
|
|
35
|
+
auth: ChatGptAuth;
|
|
36
|
+
exclude?: string[];
|
|
37
|
+
}): Promise<string | Error>;
|
|
11
38
|
/**
|
|
12
39
|
* Run the full ChatGPT OAuth browser flow with clack UI.
|
|
13
40
|
* Returns the ChatGptAuth object to be stored in credentials.json.
|
|
14
41
|
*/
|
|
15
|
-
export declare function chatGptOAuthLogin(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
* Returns the updated auth object, or an Error if refresh fails.
|
|
19
|
-
*/
|
|
42
|
+
export declare function chatGptOAuthLogin({ openInBackground, }?: {
|
|
43
|
+
openInBackground?: boolean | undefined;
|
|
44
|
+
}): Promise<ChatGptAuth>;
|
|
20
45
|
export declare function refreshChatGptToken(auth: ChatGptAuth): Promise<ChatGptAuth | Error>;
|
|
21
46
|
/**
|
|
22
47
|
* Refresh the auth object if needed and return the full updated state.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chatgpt-auth.d.ts","sourceRoot":"","sources":["../../src/cli/chatgpt-auth.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"chatgpt-auth.d.ts","sourceRoot":"","sources":["../../src/cli/chatgpt-auth.ts"],"names":[],"mappings":"AAoBA,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAoGD,gFAAgF;AAChF,wBAAgB,eAAe,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,CAKrE;AAyDD,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAKjF;AAED,wBAAgB,mBAAmB,CAAC,EAClC,YAAY,EACZ,aAAa,EACb,KAAK,EACL,IAAI,GACL,EAAE;IACD,YAAY,EAAE,MAAM,GAAG,SAAS,CAAA;IAChC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CACpB,GAAG,QAAQ,GAAG,gBAAgB,GAAG,cAAc,GAAG,UAAU,CAK5D;AAED,wBAAgB,qBAAqB,CAAC,EACpC,MAAM,EACN,IAAI,GACL,EAAE;IACD,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;CACb,GAAG,MAAM,CAGT;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CACjB,CAAA;AAID,wBAAgB,yBAAyB,CAAC,EACxC,MAAM,EACN,IAAI,EACJ,OAAY,GACb,EAAE;IACD,MAAM,EAAE,qBAAqB,EAAE,CAAA;IAC/B,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;IACxB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CACnB,GAAG,MAAM,GAAG,IAAI,CAWhB;AAED,wBAAsB,4BAA4B,CAAC,EACjD,IAAI,EACJ,OAAY,GACb,EAAE;IACD,IAAI,EAAE,WAAW,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CACnB,GAAG,OAAO,CAAC,MAAM,GAAG,KAAK,CAAC,CAiD1B;AAsMD;;;GAGG;AACH,wBAAsB,iBAAiB,CAAC,EACtC,gBAAwB,GACzB;;CAAK,GAAG,OAAO,CAAC,WAAW,CAAC,CAwE5B;AA8DD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,GAAG,KAAK,CAAC,CAUnF;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,WAAW,EACjB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,GACzC,OAAO,CAAC,WAAW,GAAG,KAAK,CAAC,CAS9B"}
|
package/dist/cli/chatgpt-auth.js
CHANGED
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
// Tokens are stored so egaki can reuse the ChatGPT login for Codex-style
|
|
7
7
|
// backend requests and refresh them when needed.
|
|
8
8
|
import { createServer } from 'node:http';
|
|
9
|
-
import { spawn } from 'node:child_process';
|
|
10
9
|
import { spinner, log, note } from '@clack/prompts';
|
|
11
|
-
import pc from '
|
|
10
|
+
import { colors as pc } from 'goke';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import { openUrlInBrowser } from './open-browser.js';
|
|
12
13
|
const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
13
14
|
const ISSUER = 'https://auth.openai.com';
|
|
14
|
-
|
|
15
|
-
const
|
|
15
|
+
// 1457 is the Codex fallback. The shared client allow-list accepts both.
|
|
16
|
+
const OAUTH_PORTS = [1455, 1457];
|
|
16
17
|
// ─── PKCE helpers ────────────────────────────────────────────────────────────
|
|
17
18
|
function randomString(length) {
|
|
18
19
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
|
|
@@ -31,10 +32,15 @@ async function generatePKCE() {
|
|
|
31
32
|
const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
|
|
32
33
|
return { verifier, challenge: base64UrlEncode(hash) };
|
|
33
34
|
}
|
|
34
|
-
function generateState() {
|
|
35
|
-
return base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)).buffer);
|
|
36
|
-
}
|
|
37
35
|
// ─── JWT parsing ─────────────────────────────────────────────────────────────
|
|
36
|
+
const jwtClaimsSchema = z.object({
|
|
37
|
+
email: z.string().optional(),
|
|
38
|
+
chatgpt_account_id: z.string().optional(),
|
|
39
|
+
'https://api.openai.com/auth': z.object({
|
|
40
|
+
chatgpt_account_id: z.string().optional(),
|
|
41
|
+
chatgpt_plan_type: z.string().optional(),
|
|
42
|
+
}).optional(),
|
|
43
|
+
});
|
|
38
44
|
function parseJwtClaims(token) {
|
|
39
45
|
const parts = token.split('.');
|
|
40
46
|
if (parts.length !== 3)
|
|
@@ -43,7 +49,7 @@ function parseJwtClaims(token) {
|
|
|
43
49
|
if (!payload)
|
|
44
50
|
return undefined;
|
|
45
51
|
try {
|
|
46
|
-
return JSON.parse(Buffer.from(payload, 'base64url').toString());
|
|
52
|
+
return jwtClaimsSchema.parse(JSON.parse(Buffer.from(payload, 'base64url').toString()));
|
|
47
53
|
}
|
|
48
54
|
catch {
|
|
49
55
|
return undefined;
|
|
@@ -85,13 +91,13 @@ export function extractPlanType(auth) {
|
|
|
85
91
|
return authClaim?.chatgpt_plan_type;
|
|
86
92
|
}
|
|
87
93
|
// ─── OAuth URL + token exchange ──────────────────────────────────────────────
|
|
88
|
-
function buildAuthorizeUrl(
|
|
94
|
+
function buildAuthorizeUrl({ challenge, state, redirectUri, }) {
|
|
89
95
|
const params = new URLSearchParams({
|
|
90
96
|
response_type: 'code',
|
|
91
97
|
client_id: CLIENT_ID,
|
|
92
|
-
redirect_uri:
|
|
98
|
+
redirect_uri: redirectUri,
|
|
93
99
|
scope: 'openid profile email offline_access api.connectors.read api.connectors.invoke',
|
|
94
|
-
code_challenge:
|
|
100
|
+
code_challenge: challenge,
|
|
95
101
|
code_challenge_method: 'S256',
|
|
96
102
|
id_token_add_organizations: 'true',
|
|
97
103
|
codex_cli_simplified_flow: 'true',
|
|
@@ -100,23 +106,94 @@ function buildAuthorizeUrl(pkce, state) {
|
|
|
100
106
|
});
|
|
101
107
|
return `${ISSUER}/oauth/authorize?${params.toString()}`;
|
|
102
108
|
}
|
|
103
|
-
async function exchangeCodeForTokens(code, verifier) {
|
|
109
|
+
async function exchangeCodeForTokens({ code, verifier, redirectUri, }) {
|
|
104
110
|
const response = await fetch(`${ISSUER}/oauth/token`, {
|
|
105
111
|
method: 'POST',
|
|
106
112
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
107
113
|
body: new URLSearchParams({
|
|
108
114
|
grant_type: 'authorization_code',
|
|
109
115
|
code,
|
|
110
|
-
redirect_uri:
|
|
116
|
+
redirect_uri: redirectUri,
|
|
111
117
|
client_id: CLIENT_ID,
|
|
112
118
|
code_verifier: verifier,
|
|
113
119
|
}).toString(),
|
|
114
120
|
});
|
|
115
121
|
if (!response.ok) {
|
|
116
|
-
|
|
122
|
+
const body = await response.text().catch(() => '');
|
|
123
|
+
throw new Error(`Token exchange failed: ${response.status}${body ? ` ${body}` : ''}. The previous ChatGPT login was not replaced.`);
|
|
117
124
|
}
|
|
118
125
|
return (await response.json());
|
|
119
126
|
}
|
|
127
|
+
export function isRevokedChatGptAccessToken(status, body) {
|
|
128
|
+
return (status === 401 &&
|
|
129
|
+
(body.includes('token_revoked') || body.includes('invalidated oauth token')));
|
|
130
|
+
}
|
|
131
|
+
export function oauthCallbackAction({ pendingState, callbackState, error, code, }) {
|
|
132
|
+
if (!pendingState || callbackState !== pendingState)
|
|
133
|
+
return 'ignore';
|
|
134
|
+
if (error)
|
|
135
|
+
return 'provider-error';
|
|
136
|
+
if (!code)
|
|
137
|
+
return 'missing-code';
|
|
138
|
+
return 'exchange';
|
|
139
|
+
}
|
|
140
|
+
export function chatGptReLoginMessage({ status, body, }) {
|
|
141
|
+
const detail = body.trim();
|
|
142
|
+
return `ChatGPT rejected the saved login (${status}${detail ? `: ${detail}` : ''}). Run: egaki login --provider chatgpt`;
|
|
143
|
+
}
|
|
144
|
+
// ChatGPT accounts reject a fixed slug such as gpt-5.4. Use the account's
|
|
145
|
+
// current models list and pick the highest-priority listed model.
|
|
146
|
+
export function pickChatGptResponsesModel({ models, plan, exclude = [], }) {
|
|
147
|
+
const ranked = models
|
|
148
|
+
.filter((model) => {
|
|
149
|
+
if (!model.slug || exclude.includes(model.slug))
|
|
150
|
+
return false;
|
|
151
|
+
if (model.visibility && model.visibility !== 'list')
|
|
152
|
+
return false;
|
|
153
|
+
if (model.supportedInApi === false)
|
|
154
|
+
return false;
|
|
155
|
+
if (plan && model.plans && !model.plans.includes(plan))
|
|
156
|
+
return false;
|
|
157
|
+
return true;
|
|
158
|
+
})
|
|
159
|
+
.sort((a, b) => (a.priority ?? 1_000) - (b.priority ?? 1_000));
|
|
160
|
+
return ranked[0]?.slug ?? null;
|
|
161
|
+
}
|
|
162
|
+
export async function resolveChatGptResponsesModel({ auth, exclude = [], }) {
|
|
163
|
+
const headers = new Headers({
|
|
164
|
+
Authorization: `Bearer ${auth.access}`,
|
|
165
|
+
});
|
|
166
|
+
if (auth.accountId)
|
|
167
|
+
headers.set('ChatGPT-Account-ID', auth.accountId);
|
|
168
|
+
const response = await fetch('https://chatgpt.com/backend-api/codex/models?client_version=0.0.0', { headers }).catch((err) => err instanceof Error ? err : new Error(String(err)));
|
|
169
|
+
if (response instanceof Error)
|
|
170
|
+
return response;
|
|
171
|
+
if (!response.ok) {
|
|
172
|
+
const body = await response.text().catch(() => '');
|
|
173
|
+
if (response.status === 401) {
|
|
174
|
+
return new Error(chatGptReLoginMessage({ status: response.status, body }));
|
|
175
|
+
}
|
|
176
|
+
return new Error(`ChatGPT model list failed: ${response.status}${body ? ` ${body}` : ''}`);
|
|
177
|
+
}
|
|
178
|
+
const json = (await response.json());
|
|
179
|
+
const slug = pickChatGptResponsesModel({
|
|
180
|
+
models: (json.models ?? []).flatMap((model) => model.slug
|
|
181
|
+
? [{
|
|
182
|
+
slug: model.slug,
|
|
183
|
+
visibility: model.visibility,
|
|
184
|
+
priority: model.priority,
|
|
185
|
+
supportedInApi: model.supported_in_api,
|
|
186
|
+
plans: model.available_in_plans,
|
|
187
|
+
}]
|
|
188
|
+
: []),
|
|
189
|
+
plan: auth.plan,
|
|
190
|
+
exclude,
|
|
191
|
+
});
|
|
192
|
+
if (!slug) {
|
|
193
|
+
return new Error('No ChatGPT model is available for image generation. Run: egaki login --provider chatgpt');
|
|
194
|
+
}
|
|
195
|
+
return slug;
|
|
196
|
+
}
|
|
120
197
|
// ─── local callback server ───────────────────────────────────────────────────
|
|
121
198
|
const HTML_SUCCESS = `<!doctype html>
|
|
122
199
|
<html><body style="font-family:system-ui;text-align:center;padding:4em">
|
|
@@ -125,10 +202,27 @@ const HTML_SUCCESS = `<!doctype html>
|
|
|
125
202
|
</body></html>`;
|
|
126
203
|
const htmlError = (error) => `<!doctype html><html><body style="font-family:system-ui;text-align:center;padding:4em">
|
|
127
204
|
<h1>Authorization Failed</h1><pre>${error}</pre></body></html>`;
|
|
205
|
+
function escapeHtml(value) {
|
|
206
|
+
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
|
207
|
+
}
|
|
208
|
+
function sendHtml({ res, status, html, }) {
|
|
209
|
+
res.statusCode = status;
|
|
210
|
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
211
|
+
res.setHeader('Connection', 'close');
|
|
212
|
+
res.end(html);
|
|
213
|
+
}
|
|
214
|
+
function failCallback(res, message) {
|
|
215
|
+
pendingOAuth?.reject(new Error(message));
|
|
216
|
+
pendingOAuth = undefined;
|
|
217
|
+
sendHtml({ res, status: 400, html: htmlError(escapeHtml(message)) });
|
|
218
|
+
}
|
|
219
|
+
const OAUTH_HOST = '127.0.0.1';
|
|
128
220
|
let oauthServer;
|
|
129
221
|
let pendingOAuth;
|
|
130
|
-
|
|
131
|
-
|
|
222
|
+
// Browser success used to be sent before token exchange. The page then said
|
|
223
|
+
// login was done while credentials.json stayed unchanged.
|
|
224
|
+
async function handleOAuthRequest(req, res) {
|
|
225
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
132
226
|
if (url.pathname !== '/auth/callback') {
|
|
133
227
|
res.statusCode = 404;
|
|
134
228
|
res.end('Not found');
|
|
@@ -138,68 +232,110 @@ function handleOAuthRequest(req, res) {
|
|
|
138
232
|
const state = url.searchParams.get('state');
|
|
139
233
|
const error = url.searchParams.get('error');
|
|
140
234
|
const errorDescription = url.searchParams.get('error_description');
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
235
|
+
const action = oauthCallbackAction({
|
|
236
|
+
pendingState: pendingOAuth?.state,
|
|
237
|
+
callbackState: state,
|
|
238
|
+
error,
|
|
239
|
+
code,
|
|
240
|
+
});
|
|
241
|
+
if (action === 'ignore') {
|
|
242
|
+
sendHtml({
|
|
243
|
+
res,
|
|
244
|
+
status: 400,
|
|
245
|
+
html: htmlError(escapeHtml('Invalid state. This callback does not match the active egaki login.')),
|
|
246
|
+
});
|
|
148
247
|
return;
|
|
149
248
|
}
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
pendingOAuth?.reject(new Error(message));
|
|
153
|
-
pendingOAuth = undefined;
|
|
154
|
-
res.statusCode = 400;
|
|
155
|
-
res.setHeader('Content-Type', 'text/html');
|
|
156
|
-
res.end(htmlError(message));
|
|
249
|
+
if (action === 'provider-error') {
|
|
250
|
+
failCallback(res, errorDescription || error || 'Authorization failed');
|
|
157
251
|
return;
|
|
158
252
|
}
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
pendingOAuth?.reject(new Error(message));
|
|
162
|
-
pendingOAuth = undefined;
|
|
163
|
-
res.statusCode = 400;
|
|
164
|
-
res.setHeader('Content-Type', 'text/html');
|
|
165
|
-
res.end(htmlError(message));
|
|
253
|
+
if (action === 'missing-code' || !code) {
|
|
254
|
+
failCallback(res, 'Missing authorization code. The previous ChatGPT login was not replaced.');
|
|
166
255
|
return;
|
|
167
256
|
}
|
|
168
257
|
const current = pendingOAuth;
|
|
258
|
+
if (!current) {
|
|
259
|
+
sendHtml({
|
|
260
|
+
res,
|
|
261
|
+
status: 400,
|
|
262
|
+
html: htmlError(escapeHtml('Invalid state. This callback does not match the active egaki login.')),
|
|
263
|
+
});
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
169
266
|
pendingOAuth = undefined;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
267
|
+
try {
|
|
268
|
+
const tokens = await exchangeCodeForTokens({
|
|
269
|
+
code,
|
|
270
|
+
verifier: current.verifier,
|
|
271
|
+
redirectUri: current.redirectUri,
|
|
272
|
+
});
|
|
273
|
+
sendHtml({ res, status: 200, html: HTML_SUCCESS });
|
|
274
|
+
current.resolve(tokens);
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
const failure = err instanceof Error ? err : new Error(String(err));
|
|
278
|
+
current.reject(failure);
|
|
279
|
+
sendHtml({ res, status: 400, html: htmlError(escapeHtml(failure.message)) });
|
|
280
|
+
}
|
|
176
281
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
282
|
+
function listen(server, port) {
|
|
283
|
+
return new Promise((resolve, reject) => {
|
|
284
|
+
const onError = (err) => {
|
|
285
|
+
server.close();
|
|
286
|
+
reject(err);
|
|
287
|
+
};
|
|
288
|
+
server.once('error', onError);
|
|
289
|
+
server.listen(port, OAUTH_HOST, () => {
|
|
290
|
+
server.removeListener('error', onError);
|
|
291
|
+
resolve();
|
|
292
|
+
});
|
|
184
293
|
});
|
|
185
294
|
}
|
|
295
|
+
// Listen on 127.0.0.1 only. The redirect host must stay localhost for the allow-list.
|
|
296
|
+
async function startOAuthServer() {
|
|
297
|
+
if (oauthServer) {
|
|
298
|
+
const address = oauthServer.address();
|
|
299
|
+
if (address && typeof address === 'object')
|
|
300
|
+
return address.port;
|
|
301
|
+
}
|
|
302
|
+
const errors = [];
|
|
303
|
+
for (const port of OAUTH_PORTS) {
|
|
304
|
+
const server = createServer((req, res) => {
|
|
305
|
+
void handleOAuthRequest(req, res);
|
|
306
|
+
});
|
|
307
|
+
try {
|
|
308
|
+
await listen(server, port);
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
312
|
+
errors.push(`${OAUTH_HOST}:${port} ${message}`);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
oauthServer = server;
|
|
316
|
+
return port;
|
|
317
|
+
}
|
|
318
|
+
throw new Error(`Could not listen for the ChatGPT login callback. ${errors.join(' ')} Close Codex or another egaki login, then run egaki login --provider chatgpt again.`);
|
|
319
|
+
}
|
|
186
320
|
async function stopOAuthServer() {
|
|
187
|
-
|
|
188
|
-
return;
|
|
189
|
-
await new Promise((resolve) => oauthServer?.close(() => resolve()));
|
|
321
|
+
const server = oauthServer;
|
|
190
322
|
oauthServer = undefined;
|
|
323
|
+
if (!server)
|
|
324
|
+
return;
|
|
325
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
191
326
|
}
|
|
192
|
-
function waitForOAuthCallback(verifier, state) {
|
|
327
|
+
function waitForOAuthCallback({ verifier, state, redirectUri, }) {
|
|
193
328
|
return new Promise((resolve, reject) => {
|
|
194
329
|
const timeout = setTimeout(() => {
|
|
195
330
|
if (pendingOAuth) {
|
|
196
331
|
pendingOAuth = undefined;
|
|
197
|
-
reject(new Error('OAuth callback timeout
|
|
332
|
+
reject(new Error('OAuth callback timeout. The browser sign-in did not reach this process, so the previous ChatGPT login was not replaced.'));
|
|
198
333
|
}
|
|
199
334
|
}, 5 * 60 * 1000);
|
|
200
335
|
pendingOAuth = {
|
|
201
336
|
verifier,
|
|
202
337
|
state,
|
|
338
|
+
redirectUri,
|
|
203
339
|
resolve: (tokens) => {
|
|
204
340
|
clearTimeout(timeout);
|
|
205
341
|
resolve(tokens);
|
|
@@ -211,32 +347,52 @@ function waitForOAuthCallback(verifier, state) {
|
|
|
211
347
|
};
|
|
212
348
|
});
|
|
213
349
|
}
|
|
214
|
-
function openBrowser(url) {
|
|
215
|
-
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
216
|
-
const child = spawn(cmd, [url], { detached: true, stdio: 'ignore' });
|
|
217
|
-
child.unref();
|
|
218
|
-
}
|
|
219
350
|
// ─── public API ──────────────────────────────────────────────────────────────
|
|
220
351
|
/**
|
|
221
352
|
* Run the full ChatGPT OAuth browser flow with clack UI.
|
|
222
353
|
* Returns the ChatGptAuth object to be stored in credentials.json.
|
|
223
354
|
*/
|
|
224
|
-
export async function chatGptOAuthLogin() {
|
|
225
|
-
await startOAuthServer();
|
|
355
|
+
export async function chatGptOAuthLogin({ openInBackground = false, } = {}) {
|
|
356
|
+
const port = await startOAuthServer();
|
|
357
|
+
// The allow-list accepts localhost, not 127.0.0.1. The listener stays on IPv4.
|
|
358
|
+
const redirectUri = `http://localhost:${port}/auth/callback`;
|
|
226
359
|
const pkce = await generatePKCE();
|
|
227
|
-
const state =
|
|
228
|
-
const authUrl = buildAuthorizeUrl(
|
|
229
|
-
|
|
360
|
+
const state = base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)).buffer);
|
|
361
|
+
const authUrl = buildAuthorizeUrl({
|
|
362
|
+
challenge: pkce.challenge,
|
|
363
|
+
state,
|
|
364
|
+
redirectUri,
|
|
365
|
+
});
|
|
366
|
+
const callbackPromise = waitForOAuthCallback({
|
|
367
|
+
verifier: pkce.verifier,
|
|
368
|
+
state,
|
|
369
|
+
redirectUri,
|
|
370
|
+
});
|
|
230
371
|
note(`Open this URL in your browser to sign in with your ChatGPT account:\n\n` +
|
|
231
372
|
` ${pc.cyan(pc.underline(authUrl))}\n\n` +
|
|
232
|
-
`${pc.dim(`Listening for callback on ${
|
|
233
|
-
|
|
373
|
+
`${pc.dim(`Listening for callback on ${redirectUri}`)}\n` +
|
|
374
|
+
`${pc.dim('The previous login stays saved until this command prints that the new login was saved.')}`, 'ChatGPT OAuth');
|
|
375
|
+
const opened = openUrlInBrowser(authUrl, {
|
|
376
|
+
allowNonInteractive: openInBackground,
|
|
377
|
+
});
|
|
378
|
+
if (openInBackground && !opened) {
|
|
379
|
+
const error = new Error('Could not open a browser for ChatGPT authorization');
|
|
380
|
+
pendingOAuth?.reject(error);
|
|
381
|
+
pendingOAuth = undefined;
|
|
382
|
+
await stopOAuthServer();
|
|
383
|
+
await callbackPromise;
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
234
386
|
const s = spinner();
|
|
235
387
|
s.start('Waiting for authorization in browser...');
|
|
236
388
|
let tokens;
|
|
237
389
|
try {
|
|
238
390
|
tokens = await callbackPromise;
|
|
239
391
|
}
|
|
392
|
+
catch (err) {
|
|
393
|
+
s.stop('ChatGPT sign-in did not save new credentials');
|
|
394
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
395
|
+
}
|
|
240
396
|
finally {
|
|
241
397
|
await stopOAuthServer();
|
|
242
398
|
}
|
|
@@ -255,30 +411,40 @@ export async function chatGptOAuthLogin() {
|
|
|
255
411
|
log.success(`Signed in as ${pc.bold(label)}` + (plan ? ` (${pc.cyan(plan)} plan)` : ''));
|
|
256
412
|
return auth;
|
|
257
413
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
* Returns the updated auth object, or an Error if refresh fails.
|
|
261
|
-
*/
|
|
262
|
-
export async function refreshChatGptToken(auth) {
|
|
414
|
+
let refreshFlight;
|
|
415
|
+
async function performChatGptTokenRefresh(auth) {
|
|
263
416
|
try {
|
|
264
417
|
const response = await fetch(`${ISSUER}/oauth/token`, {
|
|
265
418
|
method: 'POST',
|
|
266
|
-
headers: { 'Content-Type': 'application/
|
|
267
|
-
body:
|
|
419
|
+
headers: { 'Content-Type': 'application/json' },
|
|
420
|
+
body: JSON.stringify({
|
|
268
421
|
grant_type: 'refresh_token',
|
|
269
422
|
refresh_token: auth.refresh,
|
|
270
423
|
client_id: CLIENT_ID,
|
|
271
|
-
})
|
|
424
|
+
}),
|
|
272
425
|
});
|
|
273
426
|
if (!response.ok) {
|
|
274
|
-
|
|
427
|
+
const body = await response.text().catch(() => '');
|
|
428
|
+
if (response.status === 401) {
|
|
429
|
+
return new Error(chatGptReLoginMessage({ status: response.status, body }));
|
|
430
|
+
}
|
|
431
|
+
return new Error(`Token refresh failed: ${response.status}${body ? ` ${body}` : ''}`);
|
|
275
432
|
}
|
|
276
433
|
const json = (await response.json());
|
|
434
|
+
if (!json.access_token) {
|
|
435
|
+
return new Error('ChatGPT token refresh did not return an access token. Run: egaki login --provider chatgpt');
|
|
436
|
+
}
|
|
437
|
+
const tokens = {
|
|
438
|
+
id_token: json.id_token,
|
|
439
|
+
access_token: json.access_token,
|
|
440
|
+
refresh_token: json.refresh_token ?? auth.refresh,
|
|
441
|
+
expires_in: json.expires_in,
|
|
442
|
+
};
|
|
277
443
|
return {
|
|
278
444
|
...auth,
|
|
279
|
-
plan: extractPlanTypeFromTokens(
|
|
280
|
-
access:
|
|
281
|
-
refresh:
|
|
445
|
+
plan: extractPlanTypeFromTokens(tokens) ?? auth.plan,
|
|
446
|
+
access: tokens.access_token,
|
|
447
|
+
refresh: tokens.refresh_token,
|
|
282
448
|
expires: Date.now() + (json.expires_in ?? 3600) * 1000,
|
|
283
449
|
};
|
|
284
450
|
}
|
|
@@ -286,6 +452,18 @@ export async function refreshChatGptToken(auth) {
|
|
|
286
452
|
return err instanceof Error ? err : new Error(String(err));
|
|
287
453
|
}
|
|
288
454
|
}
|
|
455
|
+
export function refreshChatGptToken(auth) {
|
|
456
|
+
if (refreshFlight?.source === auth.refresh)
|
|
457
|
+
return refreshFlight.result;
|
|
458
|
+
const result = performChatGptTokenRefresh(auth);
|
|
459
|
+
refreshFlight = { source: auth.refresh, result };
|
|
460
|
+
void result.then((value) => {
|
|
461
|
+
if (value instanceof Error && refreshFlight?.result === result) {
|
|
462
|
+
refreshFlight = undefined;
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
return result;
|
|
466
|
+
}
|
|
289
467
|
/**
|
|
290
468
|
* Refresh the auth object if needed and return the full updated state.
|
|
291
469
|
*/
|
package/dist/cli/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli/cli.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli/cli.ts"],"names":[],"mappings":";AAqGA,QAAA,MAAM,GAAG,yBAAgB,CAAA;AA46CzB,OAAO,EAAE,GAAG,EAAE,CAAA"}
|