codemaxxing 1.0.17 → 1.1.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 +24 -16
- package/dist/agent.d.ts +12 -1
- package/dist/agent.js +296 -31
- package/dist/index.js +140 -43
- package/dist/ui/input-router.d.ts +42 -2
- package/dist/ui/input-router.js +70 -17
- package/dist/ui/pickers.d.ts +23 -2
- package/dist/ui/pickers.js +12 -3
- package/dist/utils/anthropic-oauth.d.ts +13 -0
- package/dist/utils/anthropic-oauth.js +171 -0
- package/dist/utils/auth.d.ts +2 -0
- package/dist/utils/auth.js +42 -3
- package/dist/utils/ollama.js +6 -1
- package/dist/utils/openai-oauth.d.ts +19 -0
- package/dist/utils/openai-oauth.js +233 -0
- package/dist/utils/responses-api.d.ts +40 -0
- package/dist/utils/responses-api.js +264 -0
- package/package.json +2 -2
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic OAuth PKCE flow
|
|
3
|
+
*
|
|
4
|
+
* Lets users log in with their Claude Pro/Max subscription (no API key needed).
|
|
5
|
+
* Uses the same OAuth flow as Claude Code CLI.
|
|
6
|
+
*/
|
|
7
|
+
import { createServer } from "http";
|
|
8
|
+
import { randomBytes, createHash } from "crypto";
|
|
9
|
+
import { exec } from "child_process";
|
|
10
|
+
import { saveCredential } from "./auth.js";
|
|
11
|
+
// ── Constants ──
|
|
12
|
+
const CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
13
|
+
const AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
|
|
14
|
+
const TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
|
|
15
|
+
const CALLBACK_HOST = "127.0.0.1";
|
|
16
|
+
const CALLBACK_PORT = 53692;
|
|
17
|
+
const CALLBACK_PATH = "/callback";
|
|
18
|
+
const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
|
|
19
|
+
const SCOPES = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
|
|
20
|
+
// ── PKCE helpers ──
|
|
21
|
+
function base64url(buf) {
|
|
22
|
+
return buf
|
|
23
|
+
.toString("base64")
|
|
24
|
+
.replace(/\+/g, "-")
|
|
25
|
+
.replace(/\//g, "_")
|
|
26
|
+
.replace(/=/g, "");
|
|
27
|
+
}
|
|
28
|
+
function generatePKCE() {
|
|
29
|
+
const verifier = base64url(randomBytes(32));
|
|
30
|
+
const challenge = base64url(createHash("sha256").update(verifier).digest());
|
|
31
|
+
return { verifier, challenge };
|
|
32
|
+
}
|
|
33
|
+
// ── Browser opener ──
|
|
34
|
+
function openBrowser(url) {
|
|
35
|
+
const cmd = process.platform === "darwin"
|
|
36
|
+
? `open "${url}"`
|
|
37
|
+
: process.platform === "win32"
|
|
38
|
+
? `start "" "${url}"`
|
|
39
|
+
: `xdg-open "${url}"`;
|
|
40
|
+
exec(cmd);
|
|
41
|
+
}
|
|
42
|
+
// ── Token refresh ──
|
|
43
|
+
export async function refreshAnthropicOAuthToken(refreshToken) {
|
|
44
|
+
const res = await fetch(TOKEN_URL, {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: { "Content-Type": "application/json" },
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
grant_type: "refresh_token",
|
|
49
|
+
client_id: CLIENT_ID,
|
|
50
|
+
refresh_token: refreshToken,
|
|
51
|
+
scope: SCOPES,
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
const errText = await res.text();
|
|
56
|
+
throw new Error(`Token refresh failed (${res.status}): ${errText}`);
|
|
57
|
+
}
|
|
58
|
+
const data = (await res.json());
|
|
59
|
+
return {
|
|
60
|
+
access: data.access_token,
|
|
61
|
+
refresh: data.refresh_token ?? refreshToken,
|
|
62
|
+
expires: Date.now() + data.expires_in * 1000,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// ── Main OAuth login flow ──
|
|
66
|
+
export async function loginAnthropicOAuth(onStatus) {
|
|
67
|
+
const { verifier, challenge } = generatePKCE();
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const server = createServer(async (req, res) => {
|
|
70
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
71
|
+
if (url.pathname !== CALLBACK_PATH) {
|
|
72
|
+
res.writeHead(404);
|
|
73
|
+
res.end("Not found");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const code = url.searchParams.get("code");
|
|
77
|
+
const returnedState = url.searchParams.get("state");
|
|
78
|
+
if (!code) {
|
|
79
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
80
|
+
res.end("<h1>Error: No authorization code received</h1><p>Please try again.</p>");
|
|
81
|
+
server.close();
|
|
82
|
+
reject(new Error("No authorization code received"));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// state should match verifier
|
|
86
|
+
if (returnedState !== verifier) {
|
|
87
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
88
|
+
res.end("<h1>Error: State mismatch</h1><p>Please try again.</p>");
|
|
89
|
+
server.close();
|
|
90
|
+
reject(new Error("OAuth state mismatch"));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
onStatus?.("Exchanging code for tokens...");
|
|
94
|
+
try {
|
|
95
|
+
const tokenRes = await fetch(TOKEN_URL, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: { "Content-Type": "application/json" },
|
|
98
|
+
body: JSON.stringify({
|
|
99
|
+
grant_type: "authorization_code",
|
|
100
|
+
client_id: CLIENT_ID,
|
|
101
|
+
code,
|
|
102
|
+
state: verifier,
|
|
103
|
+
redirect_uri: REDIRECT_URI,
|
|
104
|
+
code_verifier: verifier,
|
|
105
|
+
}),
|
|
106
|
+
});
|
|
107
|
+
if (!tokenRes.ok) {
|
|
108
|
+
const errText = await tokenRes.text();
|
|
109
|
+
throw new Error(`Token exchange failed (${tokenRes.status}): ${errText}`);
|
|
110
|
+
}
|
|
111
|
+
const tokenData = (await tokenRes.json());
|
|
112
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
113
|
+
res.end("<html><body style=\"font-family:monospace;background:#1a1a2e;color:#0ff;display:flex;justify-content:center;align-items:center;height:100vh;margin:0\"><div style=\"text-align:center\"><h1>Authenticated!</h1><p>You can close this tab and return to Codemaxxing.</p></div></body></html>");
|
|
114
|
+
server.close();
|
|
115
|
+
const expiresAt = Date.now() + tokenData.expires_in * 1000;
|
|
116
|
+
const cred = {
|
|
117
|
+
provider: "anthropic",
|
|
118
|
+
method: "oauth",
|
|
119
|
+
apiKey: tokenData.access_token,
|
|
120
|
+
baseUrl: "https://api.anthropic.com",
|
|
121
|
+
label: "Anthropic (Claude Pro/Max)",
|
|
122
|
+
refreshToken: tokenData.refresh_token,
|
|
123
|
+
oauthExpires: expiresAt,
|
|
124
|
+
createdAt: new Date().toISOString(),
|
|
125
|
+
};
|
|
126
|
+
saveCredential(cred);
|
|
127
|
+
resolve(cred);
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
res.writeHead(500, { "Content-Type": "text/html" });
|
|
131
|
+
res.end(`<h1>Error</h1><p>${err.message}</p>`);
|
|
132
|
+
server.close();
|
|
133
|
+
reject(err);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
server.listen(CALLBACK_PORT, CALLBACK_HOST, () => {
|
|
137
|
+
const params = new URLSearchParams({
|
|
138
|
+
code: "true",
|
|
139
|
+
client_id: CLIENT_ID,
|
|
140
|
+
response_type: "code",
|
|
141
|
+
redirect_uri: REDIRECT_URI,
|
|
142
|
+
scope: SCOPES,
|
|
143
|
+
code_challenge: challenge,
|
|
144
|
+
code_challenge_method: "S256",
|
|
145
|
+
state: verifier,
|
|
146
|
+
});
|
|
147
|
+
const authUrl = `${AUTHORIZE_URL}?${params.toString()}`;
|
|
148
|
+
onStatus?.("Opening browser for Claude login...");
|
|
149
|
+
try {
|
|
150
|
+
openBrowser(authUrl);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
onStatus?.(`Could not open browser. Please visit:\n${authUrl}`);
|
|
154
|
+
}
|
|
155
|
+
onStatus?.("Waiting for authorization...");
|
|
156
|
+
// Timeout after 120 seconds
|
|
157
|
+
setTimeout(() => {
|
|
158
|
+
server.close();
|
|
159
|
+
reject(new Error("OAuth timed out after 120 seconds"));
|
|
160
|
+
}, 120 * 1000);
|
|
161
|
+
});
|
|
162
|
+
server.on("error", (err) => {
|
|
163
|
+
if (err.code === "EADDRINUSE") {
|
|
164
|
+
reject(new Error(`Port ${CALLBACK_PORT} is already in use. Close other auth flows and try again.`));
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
reject(err);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
package/dist/utils/auth.d.ts
CHANGED
package/dist/utils/auth.js
CHANGED
|
@@ -17,6 +17,7 @@ import { join } from "path";
|
|
|
17
17
|
import { createServer } from "http";
|
|
18
18
|
import { randomBytes, createHash } from "crypto";
|
|
19
19
|
import { execSync, exec } from "child_process";
|
|
20
|
+
import { detectOpenAICodexOAuth } from "./openai-oauth.js";
|
|
20
21
|
// ── Paths ──
|
|
21
22
|
const CONFIG_DIR = join(homedir(), ".codemaxxing");
|
|
22
23
|
const AUTH_FILE = join(CONFIG_DIR, "auth.json");
|
|
@@ -32,15 +33,15 @@ export const PROVIDERS = [
|
|
|
32
33
|
{
|
|
33
34
|
id: "anthropic",
|
|
34
35
|
name: "Anthropic (Claude)",
|
|
35
|
-
methods: ["
|
|
36
|
-
baseUrl: "https://api.anthropic.com
|
|
36
|
+
methods: ["oauth", "api-key"],
|
|
37
|
+
baseUrl: "https://api.anthropic.com",
|
|
37
38
|
consoleUrl: "https://console.anthropic.com/settings/keys",
|
|
38
39
|
description: "Claude Opus, Sonnet, Haiku — use your subscription or API key",
|
|
39
40
|
},
|
|
40
41
|
{
|
|
41
42
|
id: "openai",
|
|
42
43
|
name: "OpenAI (ChatGPT)",
|
|
43
|
-
methods: ["
|
|
44
|
+
methods: ["oauth", "api-key"],
|
|
44
45
|
baseUrl: "https://api.openai.com/v1",
|
|
45
46
|
consoleUrl: "https://platform.openai.com/api-keys",
|
|
46
47
|
description: "GPT-4o, GPT-5, o1 — use your ChatGPT subscription or API key",
|
|
@@ -291,6 +292,16 @@ export async function anthropicSetupToken(onStatus) {
|
|
|
291
292
|
}
|
|
292
293
|
// ── OpenAI / Codex CLI Cached Token ──
|
|
293
294
|
export function detectCodexToken() {
|
|
295
|
+
// Check OpenClaw auth-profiles first (for Codex OAuth tokens)
|
|
296
|
+
try {
|
|
297
|
+
const oauthCreds = detectOpenAICodexOAuth();
|
|
298
|
+
if (oauthCreds?.access) {
|
|
299
|
+
return oauthCreds.access;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
// detection failed, fall through
|
|
304
|
+
}
|
|
294
305
|
// Codex CLI stores OAuth tokens — check common locations
|
|
295
306
|
const locations = [
|
|
296
307
|
join(homedir(), ".codex", "auth.json"),
|
|
@@ -314,6 +325,28 @@ export function detectCodexToken() {
|
|
|
314
325
|
}
|
|
315
326
|
}
|
|
316
327
|
}
|
|
328
|
+
// Codex CLI v0.18+ stores tokens in macOS Keychain
|
|
329
|
+
if (process.platform === "darwin") {
|
|
330
|
+
const keychainQueries = [
|
|
331
|
+
["security", "find-generic-password", "-s", "codex", "-w"],
|
|
332
|
+
["security", "find-generic-password", "-a", "openai", "-s", "codex", "-w"],
|
|
333
|
+
["security", "find-generic-password", "-s", "openai-codex", "-w"],
|
|
334
|
+
["security", "find-generic-password", "-l", "codex", "-w"],
|
|
335
|
+
];
|
|
336
|
+
for (const args of keychainQueries) {
|
|
337
|
+
try {
|
|
338
|
+
const token = execSync(args.join(" "), { stdio: "pipe", timeout: 3000 }).toString().trim();
|
|
339
|
+
if (token)
|
|
340
|
+
return token;
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// Final fallback: environment variable
|
|
348
|
+
if (process.env.OPENAI_API_KEY)
|
|
349
|
+
return process.env.OPENAI_API_KEY;
|
|
317
350
|
return null;
|
|
318
351
|
}
|
|
319
352
|
export function importCodexToken(onStatus) {
|
|
@@ -467,6 +500,12 @@ export function detectAvailableAuth() {
|
|
|
467
500
|
description: "Claude Code CLI detected — can link your Anthropic subscription",
|
|
468
501
|
});
|
|
469
502
|
}
|
|
503
|
+
// OpenAI: always offer OAuth login, and note if cached tokens exist
|
|
504
|
+
available.push({
|
|
505
|
+
provider: "openai",
|
|
506
|
+
method: "oauth",
|
|
507
|
+
description: "Log in with your ChatGPT subscription (browser OAuth)",
|
|
508
|
+
});
|
|
470
509
|
if (detectCodexToken()) {
|
|
471
510
|
available.push({
|
|
472
511
|
provider: "openai",
|
package/dist/utils/ollama.js
CHANGED
|
@@ -25,6 +25,11 @@ export function isOllamaInstalled() {
|
|
|
25
25
|
if (getWindowsOllamaPaths().some(p => existsSync(p)))
|
|
26
26
|
return true;
|
|
27
27
|
}
|
|
28
|
+
// Check known install paths on macOS
|
|
29
|
+
if (process.platform === "darwin") {
|
|
30
|
+
if (existsSync("/usr/local/bin/ollama") || existsSync("/opt/homebrew/bin/ollama"))
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
28
33
|
// Check if the server is responding (if server is running, Ollama is definitely installed)
|
|
29
34
|
try {
|
|
30
35
|
execSync("curl -s http://localhost:11434/api/tags", {
|
|
@@ -64,7 +69,7 @@ export async function isOllamaRunning() {
|
|
|
64
69
|
/** Get the install command for the user's OS */
|
|
65
70
|
export function getOllamaInstallCommand(os) {
|
|
66
71
|
switch (os) {
|
|
67
|
-
case "macos": return "
|
|
72
|
+
case "macos": return "curl -fsSL https://ollama.com/install.sh | sh";
|
|
68
73
|
case "linux": return "curl -fsSL https://ollama.com/install.sh | sh";
|
|
69
74
|
case "windows": return "winget install Ollama.Ollama";
|
|
70
75
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI Codex OAuth PKCE flow
|
|
3
|
+
*
|
|
4
|
+
* Lets users log in with their ChatGPT Plus/Pro subscription (no API key needed).
|
|
5
|
+
* Uses the same OAuth flow as OpenAI's Codex CLI.
|
|
6
|
+
*/
|
|
7
|
+
import { type AuthCredential } from "./auth.js";
|
|
8
|
+
export declare function detectOpenAICodexOAuth(): {
|
|
9
|
+
access: string;
|
|
10
|
+
refresh: string;
|
|
11
|
+
expires: number;
|
|
12
|
+
accountId: string;
|
|
13
|
+
} | null;
|
|
14
|
+
export declare function refreshOpenAICodexToken(refreshToken: string): Promise<{
|
|
15
|
+
access: string;
|
|
16
|
+
refresh: string;
|
|
17
|
+
expires: number;
|
|
18
|
+
}>;
|
|
19
|
+
export declare function loginOpenAICodexOAuth(onStatus?: (msg: string) => void): Promise<AuthCredential>;
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI Codex OAuth PKCE flow
|
|
3
|
+
*
|
|
4
|
+
* Lets users log in with their ChatGPT Plus/Pro subscription (no API key needed).
|
|
5
|
+
* Uses the same OAuth flow as OpenAI's Codex CLI.
|
|
6
|
+
*/
|
|
7
|
+
import { createServer } from "http";
|
|
8
|
+
import { randomBytes, createHash } from "crypto";
|
|
9
|
+
import { exec } from "child_process";
|
|
10
|
+
import { readFileSync, readdirSync, existsSync } from "fs";
|
|
11
|
+
import { homedir } from "os";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
import { saveCredential } from "./auth.js";
|
|
14
|
+
// ── Constants ──
|
|
15
|
+
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
16
|
+
const AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
|
|
17
|
+
const TOKEN_URL = "https://auth.openai.com/oauth/token";
|
|
18
|
+
const REDIRECT_URI = "http://localhost:1455/auth/callback";
|
|
19
|
+
const SCOPE = "openid profile email offline_access";
|
|
20
|
+
// ── PKCE helpers ──
|
|
21
|
+
function base64url(buf) {
|
|
22
|
+
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
23
|
+
}
|
|
24
|
+
function generatePKCE() {
|
|
25
|
+
const verifier = base64url(randomBytes(32));
|
|
26
|
+
const challenge = base64url(createHash("sha256").update(verifier).digest());
|
|
27
|
+
return { verifier, challenge };
|
|
28
|
+
}
|
|
29
|
+
// ── JWT decode (no verification — just extract payload) ──
|
|
30
|
+
function decodeJwtPayload(token) {
|
|
31
|
+
const parts = token.split(".");
|
|
32
|
+
if (parts.length < 2)
|
|
33
|
+
return {};
|
|
34
|
+
const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
35
|
+
return JSON.parse(Buffer.from(payload, "base64").toString("utf-8"));
|
|
36
|
+
}
|
|
37
|
+
// ── Browser opener ──
|
|
38
|
+
function openBrowser(url) {
|
|
39
|
+
const cmd = process.platform === "darwin"
|
|
40
|
+
? `open "${url}"`
|
|
41
|
+
: process.platform === "win32"
|
|
42
|
+
? `start "" "${url}"`
|
|
43
|
+
: `xdg-open "${url}"`;
|
|
44
|
+
exec(cmd);
|
|
45
|
+
}
|
|
46
|
+
// ── Detect existing OpenClaw auth-profiles ──
|
|
47
|
+
export function detectOpenAICodexOAuth() {
|
|
48
|
+
const openclawBase = join(homedir(), ".openclaw", "agents");
|
|
49
|
+
if (!existsSync(openclawBase))
|
|
50
|
+
return null;
|
|
51
|
+
try {
|
|
52
|
+
const agents = readdirSync(openclawBase);
|
|
53
|
+
for (const agent of agents) {
|
|
54
|
+
const profilePath = join(openclawBase, agent, "agent", "auth-profiles.json");
|
|
55
|
+
if (!existsSync(profilePath))
|
|
56
|
+
continue;
|
|
57
|
+
try {
|
|
58
|
+
const data = JSON.parse(readFileSync(profilePath, "utf-8"));
|
|
59
|
+
// auth-profiles.json has { profiles: { "openai-codex:default": { ... }, ... } }
|
|
60
|
+
const profileEntries = data?.profiles ? Object.values(data.profiles) : (Array.isArray(data) ? data : Object.values(data));
|
|
61
|
+
for (const profile of profileEntries) {
|
|
62
|
+
if (profile?.provider === "openai-codex" && profile.access) {
|
|
63
|
+
const p = profile;
|
|
64
|
+
return {
|
|
65
|
+
access: p.access,
|
|
66
|
+
refresh: p.refresh ?? "",
|
|
67
|
+
expires: p.expires ?? 0,
|
|
68
|
+
accountId: p.accountId ?? "",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// ignore
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
// ── Token refresh ──
|
|
84
|
+
export async function refreshOpenAICodexToken(refreshToken) {
|
|
85
|
+
const res = await fetch(TOKEN_URL, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
88
|
+
body: new URLSearchParams({
|
|
89
|
+
grant_type: "refresh_token",
|
|
90
|
+
client_id: CLIENT_ID,
|
|
91
|
+
refresh_token: refreshToken,
|
|
92
|
+
}).toString(),
|
|
93
|
+
});
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
const errText = await res.text();
|
|
96
|
+
throw new Error(`Token refresh failed (${res.status}): ${errText}`);
|
|
97
|
+
}
|
|
98
|
+
const data = (await res.json());
|
|
99
|
+
return {
|
|
100
|
+
access: data.access_token,
|
|
101
|
+
refresh: data.refresh_token ?? refreshToken,
|
|
102
|
+
expires: Date.now() + data.expires_in * 1000,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// ── Main OAuth login flow ──
|
|
106
|
+
export async function loginOpenAICodexOAuth(onStatus) {
|
|
107
|
+
const { verifier, challenge } = generatePKCE();
|
|
108
|
+
const state = randomBytes(16).toString("hex");
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
const server = createServer(async (req, res) => {
|
|
111
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
112
|
+
if (url.pathname !== "/auth/callback") {
|
|
113
|
+
res.writeHead(404);
|
|
114
|
+
res.end("Not found");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const code = url.searchParams.get("code");
|
|
118
|
+
const returnedState = url.searchParams.get("state");
|
|
119
|
+
if (!code) {
|
|
120
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
121
|
+
res.end("<h1>Error: No authorization code received</h1><p>Please try again.</p>");
|
|
122
|
+
server.close();
|
|
123
|
+
reject(new Error("No authorization code received"));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (returnedState !== state) {
|
|
127
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
128
|
+
res.end("<h1>Error: State mismatch</h1><p>Please try again.</p>");
|
|
129
|
+
server.close();
|
|
130
|
+
reject(new Error("OAuth state mismatch"));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
onStatus?.("Exchanging code for tokens...");
|
|
134
|
+
try {
|
|
135
|
+
const tokenRes = await fetch(TOKEN_URL, {
|
|
136
|
+
method: "POST",
|
|
137
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
138
|
+
body: new URLSearchParams({
|
|
139
|
+
grant_type: "authorization_code",
|
|
140
|
+
client_id: CLIENT_ID,
|
|
141
|
+
code,
|
|
142
|
+
code_verifier: verifier,
|
|
143
|
+
redirect_uri: REDIRECT_URI,
|
|
144
|
+
}).toString(),
|
|
145
|
+
});
|
|
146
|
+
if (!tokenRes.ok) {
|
|
147
|
+
const errText = await tokenRes.text();
|
|
148
|
+
throw new Error(`Token exchange failed (${tokenRes.status}): ${errText}`);
|
|
149
|
+
}
|
|
150
|
+
const tokenData = (await tokenRes.json());
|
|
151
|
+
// Extract accountId from JWT
|
|
152
|
+
let accountId = "";
|
|
153
|
+
try {
|
|
154
|
+
const payload = decodeJwtPayload(tokenData.access_token);
|
|
155
|
+
const authClaim = payload["https://api.openai.com/auth"];
|
|
156
|
+
if (authClaim?.chatgpt_account_id) {
|
|
157
|
+
accountId = authClaim.chatgpt_account_id;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// non-fatal — accountId is optional
|
|
162
|
+
}
|
|
163
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
164
|
+
res.end(`
|
|
165
|
+
<html>
|
|
166
|
+
<body style="font-family: monospace; background: #1a1a2e; color: #0ff; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0;">
|
|
167
|
+
<div style="text-align: center;">
|
|
168
|
+
<h1>Authenticated!</h1>
|
|
169
|
+
<p>You can close this tab and return to Codemaxxing.</p>
|
|
170
|
+
</div>
|
|
171
|
+
</body>
|
|
172
|
+
</html>
|
|
173
|
+
`);
|
|
174
|
+
server.close();
|
|
175
|
+
const expiresAt = Date.now() + tokenData.expires_in * 1000;
|
|
176
|
+
const cred = {
|
|
177
|
+
provider: "openai",
|
|
178
|
+
method: "oauth",
|
|
179
|
+
apiKey: tokenData.access_token,
|
|
180
|
+
baseUrl: "https://chatgpt.com/backend-api",
|
|
181
|
+
label: "OpenAI (ChatGPT subscription)",
|
|
182
|
+
refreshToken: tokenData.refresh_token,
|
|
183
|
+
oauthExpires: expiresAt,
|
|
184
|
+
createdAt: new Date().toISOString(),
|
|
185
|
+
};
|
|
186
|
+
saveCredential(cred);
|
|
187
|
+
resolve(cred);
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
res.writeHead(500, { "Content-Type": "text/html" });
|
|
191
|
+
res.end(`<h1>Error</h1><p>${err.message}</p>`);
|
|
192
|
+
server.close();
|
|
193
|
+
reject(err);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
server.listen(1455, "127.0.0.1", () => {
|
|
197
|
+
const params = new URLSearchParams({
|
|
198
|
+
response_type: "code",
|
|
199
|
+
client_id: CLIENT_ID,
|
|
200
|
+
redirect_uri: REDIRECT_URI,
|
|
201
|
+
scope: SCOPE,
|
|
202
|
+
code_challenge: challenge,
|
|
203
|
+
code_challenge_method: "S256",
|
|
204
|
+
state,
|
|
205
|
+
id_token_add_organizations: "true",
|
|
206
|
+
codex_cli_simplified_flow: "true",
|
|
207
|
+
originator: "codemaxxing",
|
|
208
|
+
});
|
|
209
|
+
const authUrl = `${AUTHORIZE_URL}?${params.toString()}`;
|
|
210
|
+
onStatus?.("Opening browser for ChatGPT login...");
|
|
211
|
+
try {
|
|
212
|
+
openBrowser(authUrl);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
onStatus?.(`Could not open browser. Please visit:\n${authUrl}`);
|
|
216
|
+
}
|
|
217
|
+
onStatus?.("Waiting for authorization...");
|
|
218
|
+
// Timeout after 60 seconds
|
|
219
|
+
setTimeout(() => {
|
|
220
|
+
server.close();
|
|
221
|
+
reject(new Error("OAuth timed out after 60 seconds"));
|
|
222
|
+
}, 60 * 1000);
|
|
223
|
+
});
|
|
224
|
+
server.on("error", (err) => {
|
|
225
|
+
if (err.code === "EADDRINUSE") {
|
|
226
|
+
reject(new Error("Port 1455 is already in use. Close other auth flows and try again."));
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
reject(err);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI Codex Responses API handler
|
|
3
|
+
*
|
|
4
|
+
* Uses the ChatGPT backend endpoint (https://chatgpt.com/backend-api/codex/responses)
|
|
5
|
+
* which is what Codex CLI, OpenClaw, and other tools use with ChatGPT Plus OAuth tokens.
|
|
6
|
+
*
|
|
7
|
+
* This endpoint supports the Responses API format but is separate from api.openai.com.
|
|
8
|
+
* Standard API keys use api.openai.com/v1/responses; Codex OAuth tokens use this.
|
|
9
|
+
*/
|
|
10
|
+
export interface ResponsesAPIOptions {
|
|
11
|
+
baseUrl: string;
|
|
12
|
+
apiKey: string;
|
|
13
|
+
model: string;
|
|
14
|
+
maxTokens: number;
|
|
15
|
+
systemPrompt: string;
|
|
16
|
+
messages: any[];
|
|
17
|
+
tools: any[];
|
|
18
|
+
onToken?: (token: string) => void;
|
|
19
|
+
onToolCall?: (name: string, args: Record<string, unknown>) => void;
|
|
20
|
+
}
|
|
21
|
+
interface ToolCall {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
input: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Execute a chat request using the Codex Responses API endpoint
|
|
28
|
+
* Streams text + handles tool calls
|
|
29
|
+
*/
|
|
30
|
+
export declare function chatWithResponsesAPI(options: ResponsesAPIOptions): Promise<{
|
|
31
|
+
contentText: string;
|
|
32
|
+
toolCalls: ToolCall[];
|
|
33
|
+
promptTokens: number;
|
|
34
|
+
completionTokens: number;
|
|
35
|
+
}>;
|
|
36
|
+
/**
|
|
37
|
+
* Determine if a model should use the Responses API
|
|
38
|
+
*/
|
|
39
|
+
export declare function shouldUseResponsesAPI(model: string): boolean;
|
|
40
|
+
export {};
|