ima2-gen 3.18.0 → 3.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/commands/doctor.js +13 -8
- package/bin/commands/gpt.js +216 -0
- package/bin/ima2.js +89 -53
- package/bin/lib/doctor-providers.js +19 -4
- package/docs/API.md +6 -3
- package/docs/migration/runtime-test-inventory.md +4 -1
- package/lib/authStatus.js +93 -0
- package/lib/chatgptAuth.js +236 -0
- package/lib/chatgptLogin.js +257 -0
- package/lib/codexDetect.js +16 -10
- package/lib/oauthLauncher.js +32 -0
- package/lib/oauthProxy/runtime.js +3 -0
- package/node_modules/openai-oauth/dist/chunk-2AENSHRT.js +84 -14
- package/node_modules/openai-oauth/package.json +2 -2
- package/package.json +3 -3
- package/routes/auth.js +111 -204
- package/routes/health.js +23 -9
- package/routes/quota.js +4 -9
- package/server.js +56 -11
- package/ui/dist/.vite/manifest.json +64 -64
- package/ui/dist/assets/{AgentWorkspace-CxbtxZYs.js → AgentWorkspace-C6ke6Eo4.js} +1 -1
- package/ui/dist/assets/{App-x_fzCXKb.js → App-pMN1OjNW.js} +8 -8
- package/ui/dist/assets/{AssetGenWorkspace-BIYh26wL.js → AssetGenWorkspace-DzEfpUy_.js} +2 -2
- package/ui/dist/assets/{AssetMediaLightbox-ordAqhjX.js → AssetMediaLightbox-Bx0azN0S.js} +1 -1
- package/ui/dist/assets/AssetsWorkspace-CHw1u9VC.js +1 -0
- package/ui/dist/assets/{CardNewsWorkspace-BfR-FMwo.js → CardNewsWorkspace-DsjqhaYO.js} +2 -2
- package/ui/dist/assets/{GenerationRequestLogPanel-CaVBCWhI.js → GenerationRequestLogPanel-EYEiHIr9.js} +1 -1
- package/ui/dist/assets/{HomeWorkspace-x-oJypd7.js → HomeWorkspace-DbnY1YF2.js} +1 -1
- package/ui/dist/assets/{KeyingPanel-SsAIeDy3.js → KeyingPanel-8J_RMrxg.js} +1 -1
- package/ui/dist/assets/{NodeCanvas-B1R-fOYq.js → NodeCanvas-DmjbNcTa.js} +1 -1
- package/ui/dist/assets/{PromptBuilderPanel-gGHOSb6l.js → PromptBuilderPanel-CL34Jpso.js} +1 -1
- package/ui/dist/assets/{PromptImportDialog-B5Wv2cie.js → PromptImportDialog-B24cvEkK.js} +2 -2
- package/ui/dist/assets/{PromptImportDiscoverySection-B5AdpMnV.js → PromptImportDiscoverySection-Bbw1_J_3.js} +1 -1
- package/ui/dist/assets/{PromptImportFolderSection-DgIWI0j2.js → PromptImportFolderSection-CMpxuE8e.js} +1 -1
- package/ui/dist/assets/PromptLibraryPanel-Ct98TJ61.js +2 -0
- package/ui/dist/assets/SettingsWorkspace-DL9Tm90P.js +1 -0
- package/ui/dist/assets/{SpriteRecipeWorkspace-ghZGUlM8.js → SpriteRecipeWorkspace-BJUH1ddP.js} +1 -1
- package/ui/dist/assets/{index-B-iNVdeL.js → index-CHeVGnoy.js} +6 -6
- package/ui/dist/assets/{index-Co3ZHk9P.js → index-D_LpLGWV.js} +3 -3
- package/ui/dist/assets/{pptxgen.es-D6s2Ip8Q.js → pptxgen.es-s4xfPXLT.js} +1 -1
- package/ui/dist/assets/{promptBuilderStore-CQxHJQz4.js → promptBuilderStore-DQR29LwN.js} +1 -1
- package/ui/dist/assets/useAgentDialogFocus-DAeS9IWG.js +1 -0
- package/ui/dist/index.html +1 -1
- package/vendor/openai-oauth-1.0.2-ima2.2.tgz +0 -0
- package/ui/dist/assets/AssetsWorkspace-BuiwpbQQ.js +0 -1
- package/ui/dist/assets/PromptLibraryPanel-CGcqRRsJ.js +0 -2
- package/ui/dist/assets/SettingsWorkspace-BhbAnrYw.js +0 -1
- package/ui/dist/assets/useAgentDialogFocus-ALfcs_Em.js +0 -1
- package/vendor/openai-oauth-1.0.2-ima2.1.tgz +0 -0
package/bin/commands/doctor.js
CHANGED
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from "url";
|
|
|
4
4
|
import { buildHardeningDoctorLines } from "../lib/doctor-checks.js";
|
|
5
5
|
import { buildStorageDoctorLines } from "../lib/storage-doctor.js";
|
|
6
6
|
import { detectCodexAuth } from "../../lib/codexDetect.js";
|
|
7
|
+
import { gptAuthStatus } from "../../lib/authStatus.js";
|
|
7
8
|
import { runImageDoctorProbe } from "../../lib/responsesDoctor.js";
|
|
8
9
|
import { config as runtimeConfig } from "../../config.js";
|
|
9
10
|
import { exitFlushed } from "../lib/output.js";
|
|
@@ -187,16 +188,20 @@ async function standardDoctor(args = []) {
|
|
|
187
188
|
console.log("");
|
|
188
189
|
for (const line of storageLines)
|
|
189
190
|
console.log(line);
|
|
190
|
-
const
|
|
191
|
-
if (fileConfig.provider === "oauth" && !
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
191
|
+
const gpt = gptAuthStatus();
|
|
192
|
+
if (fileConfig.provider === "oauth" && !gpt.loggedIn) {
|
|
193
|
+
const keyringOnly = gpt.reason === "no_session" && detectCodexAuth().probe === "authed";
|
|
194
|
+
console.log(keyringOnly
|
|
195
|
+
? " ✗ Codex is keyring-authenticated, but GPT OAuth needs a session file; run 'ima2 login'"
|
|
196
|
+
: ` ✗ GPT OAuth ${gpt.reason === "no_refresh_token" ? "session cannot refresh" : "has no ChatGPT session"}; run 'ima2 login'`);
|
|
195
197
|
fail++;
|
|
196
198
|
}
|
|
197
|
-
else if (
|
|
198
|
-
console.log("
|
|
199
|
-
|
|
199
|
+
else if (gpt.loggedIn) {
|
|
200
|
+
console.log(` ${gpt.health === "warning" ? "⚠" : "✓"} GPT OAuth ChatGPT session ready (${gpt.source}${gpt.email ? `, ${gpt.email}` : ""})`);
|
|
201
|
+
if (gpt.health === "warning")
|
|
202
|
+
console.log(" Shared with the Codex CLI; run 'ima2 login' for an ima2-owned session");
|
|
203
|
+
else
|
|
204
|
+
ok++;
|
|
200
205
|
}
|
|
201
206
|
const providerLines = buildProviderDoctorLines(fileConfig);
|
|
202
207
|
console.log("");
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ima2 gpt — ChatGPT (GPT OAuth) session management.
|
|
3
|
+
*
|
|
4
|
+
* The session lives in ima2's own store (lib/chatgptAuth.ts, ~/.ima2/chatgpt-auth.json) and
|
|
5
|
+
* is read by the openai-oauth proxy. Codex CLI files are still accepted as a fallback, but
|
|
6
|
+
* logging in here gives ima2 an independent session that the Codex CLI never refreshes.
|
|
7
|
+
*/
|
|
8
|
+
import { parseArgs } from "../lib/args.js";
|
|
9
|
+
import { findRunningServer, request } from "../lib/client.js";
|
|
10
|
+
import { color, die, json, out } from "../lib/output.js";
|
|
11
|
+
import { openUrl } from "../lib/platform.js";
|
|
12
|
+
import { chatgptAuthFilePath, clearChatgptCredentials, resolveChatgptSession, } from "../../lib/chatgptAuth.js";
|
|
13
|
+
import { runChatgptLogin } from "../../lib/chatgptLogin.js";
|
|
14
|
+
import { gptAuthStatus } from "../../lib/authStatus.js";
|
|
15
|
+
const HELP = `
|
|
16
|
+
ima2 gpt <subcommand> [options]
|
|
17
|
+
|
|
18
|
+
Manage the ChatGPT (GPT OAuth) session used by the GPT image lane.
|
|
19
|
+
|
|
20
|
+
Subcommands:
|
|
21
|
+
login [--device] Log in with your ChatGPT account (browser, or device code)
|
|
22
|
+
status [--json] Show which session the GPT OAuth proxy uses
|
|
23
|
+
logout Remove ima2's stored ChatGPT session
|
|
24
|
+
|
|
25
|
+
Notes:
|
|
26
|
+
The session is stored in ${"~"}/.ima2/chatgpt-auth.json. Existing Codex CLI sessions
|
|
27
|
+
(~/.codex/auth.json) are used only when ima2 has none of its own.
|
|
28
|
+
`;
|
|
29
|
+
const LOGIN_HELP = `
|
|
30
|
+
ima2 gpt login [--device] [--no-open]
|
|
31
|
+
|
|
32
|
+
Log in with your ChatGPT account. The default opens the browser and receives the
|
|
33
|
+
callback on http://localhost:1455. Use --device on a headless or remote machine:
|
|
34
|
+
it prints a code to enter at https://auth.openai.com/codex/device.
|
|
35
|
+
|
|
36
|
+
Options:
|
|
37
|
+
--device Use the device-code flow instead of the browser callback
|
|
38
|
+
--no-open Print the URL instead of opening a browser
|
|
39
|
+
-h, --help Show this help
|
|
40
|
+
`;
|
|
41
|
+
const STATUS_HELP = `
|
|
42
|
+
ima2 gpt status [--json]
|
|
43
|
+
|
|
44
|
+
Show the ChatGPT session the GPT OAuth proxy will read: source file, account,
|
|
45
|
+
plan, access-token expiry, and whether a refresh token is present.
|
|
46
|
+
|
|
47
|
+
Options:
|
|
48
|
+
--json Print the status as one JSON object
|
|
49
|
+
-h, --help Show this help
|
|
50
|
+
`;
|
|
51
|
+
const LOGOUT_HELP = `
|
|
52
|
+
ima2 gpt logout
|
|
53
|
+
|
|
54
|
+
Remove ima2's stored ChatGPT session. Codex CLI files are never touched.
|
|
55
|
+
|
|
56
|
+
Options:
|
|
57
|
+
-h, --help Show this help
|
|
58
|
+
`;
|
|
59
|
+
const LOGIN_SPEC = {
|
|
60
|
+
flags: {
|
|
61
|
+
device: { type: "boolean" },
|
|
62
|
+
"no-open": { type: "boolean" },
|
|
63
|
+
help: { short: "h", type: "boolean" },
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
const STATUS_SPEC = { flags: { json: { type: "boolean" }, help: { short: "h", type: "boolean" } } };
|
|
67
|
+
const HELP_ONLY_SPEC = { flags: { help: { short: "h", type: "boolean" } } };
|
|
68
|
+
function rejectUnknownFlags(args) {
|
|
69
|
+
if (args._unknown?.length)
|
|
70
|
+
die(2, `unknown option: ${args._unknown[0]}`);
|
|
71
|
+
}
|
|
72
|
+
/** Relative rendering of an absolute expiry, e.g. "in 58m" or "expired 2h ago". */
|
|
73
|
+
export function relativeTime(epochMs) {
|
|
74
|
+
const deltaMs = epochMs - Date.now();
|
|
75
|
+
const minutes = Math.round(Math.abs(deltaMs) / 60_000);
|
|
76
|
+
const span = minutes < 60 ? `${minutes}m` : minutes < 1440 ? `${Math.round(minutes / 60)}h` : `${Math.round(minutes / 1440)}d`;
|
|
77
|
+
return deltaMs >= 0 ? `in ${span}` : `expired ${span} ago`;
|
|
78
|
+
}
|
|
79
|
+
const HEALTH_LABEL = {
|
|
80
|
+
healthy: "healthy",
|
|
81
|
+
warning: "warning",
|
|
82
|
+
reauth_required: "login required",
|
|
83
|
+
not_logged_in: "not logged in",
|
|
84
|
+
};
|
|
85
|
+
function healthMark(health) {
|
|
86
|
+
if (health === "healthy")
|
|
87
|
+
return color.green("✓");
|
|
88
|
+
if (health === "warning")
|
|
89
|
+
return color.yellow("⚠");
|
|
90
|
+
if (health === "reauth_required")
|
|
91
|
+
return color.red("✗");
|
|
92
|
+
return color.yellow("○");
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Human status block shared by `ima2 status`, `ima2 gpt status` and `ima2 grok`-style output:
|
|
96
|
+
* verdict, account, source, expiry, then the note and the one action that fixes it.
|
|
97
|
+
*/
|
|
98
|
+
export function authStatusLines(label, status, file, indent = " ") {
|
|
99
|
+
const who = [status.email, status.plan].filter(Boolean).join(" · ");
|
|
100
|
+
const lines = [`${healthMark(status.health)} ${label}: ${HEALTH_LABEL[status.health]}${who ? ` (${who})` : ""}`];
|
|
101
|
+
if (status.source)
|
|
102
|
+
lines.push(`${indent}source: ${status.source}`);
|
|
103
|
+
if (status.accountId)
|
|
104
|
+
lines.push(`${indent}account: ${status.accountId}`);
|
|
105
|
+
if (status.loggedIn || status.expiresAt) {
|
|
106
|
+
const expiry = status.expiresAt ? relativeTime(Date.parse(status.expiresAt)) : "expiry unknown";
|
|
107
|
+
lines.push(`${indent}access token: ${expiry}, refresh token ${status.refreshable ? "present" : "absent"}`);
|
|
108
|
+
}
|
|
109
|
+
if (file)
|
|
110
|
+
lines.push(color.dim(`${indent}file: ${file}`));
|
|
111
|
+
if (status.note)
|
|
112
|
+
lines.push(color.dim(`${indent}${status.note}`));
|
|
113
|
+
if (status.action)
|
|
114
|
+
lines.push(`${indent}Action: run '${status.action}'`);
|
|
115
|
+
return lines;
|
|
116
|
+
}
|
|
117
|
+
function printPrompt(prompt, openBrowser) {
|
|
118
|
+
out("");
|
|
119
|
+
if (prompt.flow === "device") {
|
|
120
|
+
out(` Open ${color.cyan(prompt.url)}`);
|
|
121
|
+
out(` and enter the code ${color.bold(prompt.userCode ?? "")}`);
|
|
122
|
+
out(color.dim(` The code expires in ${Math.round(prompt.expiresIn / 60)} minutes.`));
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
const opened = openBrowser ? openUrl(prompt.url).ok : false;
|
|
126
|
+
out(opened ? " Opened the ChatGPT login page in your browser." : " Open this URL in a browser on this machine:");
|
|
127
|
+
out(` ${color.cyan(prompt.url)}`);
|
|
128
|
+
out(color.dim(" Waiting for the callback on http://localhost:1455 (5 minutes)."));
|
|
129
|
+
}
|
|
130
|
+
out("");
|
|
131
|
+
}
|
|
132
|
+
/** Tell a running server to pick the new session up. Best effort: an old server may lack the route. */
|
|
133
|
+
export async function notifyServerOfGptLogin() {
|
|
134
|
+
try {
|
|
135
|
+
const server = await findRunningServer({ includeEnv: false });
|
|
136
|
+
if (!server)
|
|
137
|
+
return false;
|
|
138
|
+
await request(server.base, "/api/oauth/restart", { method: "POST", body: {} });
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
export async function gptLogin(options = {}) {
|
|
146
|
+
const openBrowser = options.open !== false;
|
|
147
|
+
return runChatgptLogin({
|
|
148
|
+
flow: options.device ? "device" : "browser",
|
|
149
|
+
onPrompt: (prompt) => printPrompt(prompt, openBrowser),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
async function loginCmd(argv) {
|
|
153
|
+
const args = parseArgs(argv, LOGIN_SPEC);
|
|
154
|
+
if (args.help) {
|
|
155
|
+
out(LOGIN_HELP);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
rejectUnknownFlags(args);
|
|
159
|
+
let session;
|
|
160
|
+
try {
|
|
161
|
+
session = await gptLogin({ device: args.device === true, open: args["no-open"] !== true });
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
die(1, `ChatGPT login failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
165
|
+
}
|
|
166
|
+
out(color.green("✓ ") + `ChatGPT session saved to ${session.path}${session.email ? ` (${session.email})` : ""}`);
|
|
167
|
+
if (await notifyServerOfGptLogin())
|
|
168
|
+
out(color.dim(" The running ima2 server reloaded the GPT OAuth proxy."));
|
|
169
|
+
}
|
|
170
|
+
function statusCmd(argv) {
|
|
171
|
+
const args = parseArgs(argv, STATUS_SPEC);
|
|
172
|
+
if (args.help) {
|
|
173
|
+
out(STATUS_HELP);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
rejectUnknownFlags(args);
|
|
177
|
+
const session = resolveChatgptSession();
|
|
178
|
+
const status = gptAuthStatus(session);
|
|
179
|
+
if (args.json) {
|
|
180
|
+
json({ ...status, ...(session ? { file: session.path } : {}) });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
for (const line of authStatusLines("ChatGPT (GPT OAuth)", status, session?.path))
|
|
184
|
+
out(line);
|
|
185
|
+
}
|
|
186
|
+
async function logoutCmd(argv) {
|
|
187
|
+
const args = parseArgs(argv, HELP_ONLY_SPEC);
|
|
188
|
+
if (args.help) {
|
|
189
|
+
out(LOGOUT_HELP);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
rejectUnknownFlags(args);
|
|
193
|
+
const removed = clearChatgptCredentials();
|
|
194
|
+
out(removed
|
|
195
|
+
? color.green("✓ ") + `Removed ${chatgptAuthFilePath()}`
|
|
196
|
+
: color.yellow("○ ") + "ima2 had no stored ChatGPT session");
|
|
197
|
+
const fallback = resolveChatgptSession();
|
|
198
|
+
if (fallback)
|
|
199
|
+
out(color.dim(` The GPT OAuth proxy will now use ${fallback.path} (${fallback.source}).`));
|
|
200
|
+
await notifyServerOfGptLogin();
|
|
201
|
+
}
|
|
202
|
+
export default async function gptCmd(argv) {
|
|
203
|
+
const sub = argv[0];
|
|
204
|
+
if (!sub || sub === "--help" || sub === "-h") {
|
|
205
|
+
out(HELP);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const rest = argv.slice(1);
|
|
209
|
+
if (sub === "login")
|
|
210
|
+
return loginCmd(rest);
|
|
211
|
+
if (sub === "status")
|
|
212
|
+
return statusCmd(rest);
|
|
213
|
+
if (sub === "logout")
|
|
214
|
+
return logoutCmd(rest);
|
|
215
|
+
die(2, `unknown subcommand '${sub}'. Run 'ima2 gpt --help'.`);
|
|
216
|
+
}
|
package/bin/ima2.js
CHANGED
|
@@ -9,8 +9,8 @@ import { spawn, execFileSync } from "child_process";
|
|
|
9
9
|
import { confirmDestructiveAction } from "./lib/destructive-confirm.js";
|
|
10
10
|
import { openUrl, killProcessTree } from "./lib/platform.js";
|
|
11
11
|
import { ensureFreshUiDist } from "./lib/ui-build.js";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
12
|
+
import { detectCodexAuth } from "../lib/codexDetect.js";
|
|
13
|
+
import { resolveChatgptSession } from "../lib/chatgptAuth.js";
|
|
14
14
|
import { errInfo } from "../lib/errInfo.js";
|
|
15
15
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
16
|
const ROOT = join(__dirname, "..");
|
|
@@ -51,12 +51,12 @@ const LEGACY_CONFIG_FILE = join(ROOT, ".ima2", "config.json");
|
|
|
51
51
|
function runSelf(args) {
|
|
52
52
|
execFileSync(process.execPath, [join(ROOT, "bin", "ima2.js"), ...args], { stdio: "inherit" });
|
|
53
53
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
/** Native ChatGPT login into ima2's own store (lib/chatgptLogin.ts); no Codex CLI child. */
|
|
55
|
+
async function runGptLogin() {
|
|
56
|
+
const { gptLogin, notifyServerOfGptLogin } = await import("./commands/gpt.js");
|
|
57
|
+
const session = await gptLogin();
|
|
58
|
+
console.log(`\n ChatGPT session saved to ${session.path}${session.email ? ` (${session.email})` : ""}\n`);
|
|
59
|
+
await notifyServerOfGptLogin();
|
|
60
60
|
}
|
|
61
61
|
function loadConfig() {
|
|
62
62
|
if (existsSync(CONFIG_FILE)) {
|
|
@@ -132,22 +132,19 @@ async function setup() {
|
|
|
132
132
|
saveConfig(config);
|
|
133
133
|
console.log("\n Setting up both GPT OAuth + Grok OAuth...\n");
|
|
134
134
|
// GPT OAuth
|
|
135
|
-
const
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
135
|
+
const existing = resolveChatgptSession();
|
|
136
|
+
if (existing?.source === "ima2" && existing.refreshable) {
|
|
137
|
+
console.log(` GPT OAuth session found (${existing.email ?? existing.path}).\n`);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
140
|
console.log(" Running GPT OAuth login...\n");
|
|
141
141
|
try {
|
|
142
|
-
|
|
142
|
+
await runGptLogin();
|
|
143
143
|
}
|
|
144
|
-
catch {
|
|
145
|
-
console.log(
|
|
144
|
+
catch (e) {
|
|
145
|
+
console.log(`\n GPT login failed: ${e.message}\n Continuing with Grok...\n`);
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
|
-
else {
|
|
149
|
-
console.log(` GPT OAuth session found.\n`);
|
|
150
|
-
}
|
|
151
148
|
// Grok OAuth
|
|
152
149
|
console.log(" Running Grok OAuth login...\n");
|
|
153
150
|
try {
|
|
@@ -166,25 +163,23 @@ async function setup() {
|
|
|
166
163
|
delete config.apiKey;
|
|
167
164
|
saveConfig(config);
|
|
168
165
|
console.log("\n Starting GPT OAuth login...\n");
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
console.log(" Running 'codex login' — follow the browser prompt.\n");
|
|
166
|
+
// Setup reuses an ima2 session it already has; 'ima2 login' always signs in again.
|
|
167
|
+
const existing = resolveChatgptSession();
|
|
168
|
+
const reuse = existing?.source === "ima2" && existing.refreshable;
|
|
169
|
+
if (!reuse) {
|
|
170
|
+
console.log(" Signing in with your ChatGPT account — follow the browser prompt.\n");
|
|
176
171
|
try {
|
|
177
|
-
|
|
172
|
+
await runGptLogin();
|
|
178
173
|
}
|
|
179
|
-
catch {
|
|
180
|
-
console.log(
|
|
174
|
+
catch (e) {
|
|
175
|
+
console.log(`\n Login failed: ${e.message}`);
|
|
176
|
+
console.log(" Retry with 'ima2 login' (or 'ima2 login --device' on a headless machine).\n");
|
|
181
177
|
rl.close();
|
|
182
178
|
exitFlushed(1);
|
|
183
179
|
}
|
|
184
180
|
}
|
|
185
181
|
else {
|
|
186
|
-
|
|
187
|
-
console.log(` Existing GPT OAuth session found (${how}).\n`);
|
|
182
|
+
console.log(` Existing GPT OAuth session found (${existing.email ?? existing.path}).\n`);
|
|
188
183
|
}
|
|
189
184
|
saveConfig(config);
|
|
190
185
|
console.log(" GPT OAuth configured. Starting server...\n");
|
|
@@ -278,27 +273,54 @@ async function showStatus() {
|
|
|
278
273
|
console.log(" Status: not configured");
|
|
279
274
|
console.log(" Run 'ima2 setup' to configure.\n");
|
|
280
275
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if (
|
|
287
|
-
console.log(
|
|
288
|
-
}
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
console.log(
|
|
296
|
-
}
|
|
297
|
-
else
|
|
298
|
-
console.log("
|
|
276
|
+
const report = await buildAuthReport();
|
|
277
|
+
console.log(" Logins");
|
|
278
|
+
const { authStatusLines } = await import("./commands/gpt.js");
|
|
279
|
+
for (const line of authStatusLines("GPT OAuth (ChatGPT)", report.gpt, report.gptFile, " "))
|
|
280
|
+
console.log(` ${line}`);
|
|
281
|
+
if (report.keyringOnly) {
|
|
282
|
+
console.log(" Codex CLI is signed in through the OS keyring only; the GPT OAuth proxy cannot read that.");
|
|
283
|
+
}
|
|
284
|
+
for (const line of authStatusLines("Grok OAuth (xAI)", report.grok, undefined, " "))
|
|
285
|
+
console.log(` ${line}`);
|
|
286
|
+
console.log("");
|
|
287
|
+
if (report.server) {
|
|
288
|
+
const proxy = report.server.proxy ?? "unknown";
|
|
289
|
+
const mark = proxy === "ready" ? "✓" : proxy === "starting" ? "…" : "✗";
|
|
290
|
+
console.log(` Server ${report.server.url}: GPT OAuth proxy ${mark} ${proxy}`);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
console.log(" Server: not running (start it with 'ima2 serve')");
|
|
299
294
|
}
|
|
300
295
|
console.log("");
|
|
301
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* Auth verdicts for `ima2 status`. When a server is advertised, its live proxy verdict is
|
|
299
|
+
* folded in: a session file can look fine while ChatGPT has already revoked it.
|
|
300
|
+
*/
|
|
301
|
+
async function buildAuthReport() {
|
|
302
|
+
const { gptAuthStatus, grokAuthStatus } = await import("../lib/authStatus.js");
|
|
303
|
+
const { resolveChatgptSession } = await import("../lib/chatgptAuth.js");
|
|
304
|
+
const session = resolveChatgptSession();
|
|
305
|
+
let server = null;
|
|
306
|
+
const url = advertisedServerUrl();
|
|
307
|
+
if (url) {
|
|
308
|
+
try {
|
|
309
|
+
const res = await fetch(`${url}/api/oauth/status`, { signal: AbortSignal.timeout(2500) });
|
|
310
|
+
const body = await res.json();
|
|
311
|
+
const proxy = body.status;
|
|
312
|
+
server = proxy === "ready" || proxy === "auth_required" || proxy === "starting" || proxy === "offline"
|
|
313
|
+
? { url, proxy }
|
|
314
|
+
: { url };
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
server = null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const gpt = gptAuthStatus(session, server?.proxy ? { proxyStatus: server.proxy } : {});
|
|
321
|
+
const keyringOnly = !session && detectCodexAuth().probe === "authed";
|
|
322
|
+
return { gpt, gptFile: session?.path, grok: grokAuthStatus(), keyringOnly, server };
|
|
323
|
+
}
|
|
302
324
|
function openBrowser() {
|
|
303
325
|
const url = advertisedServerUrl() || `http://localhost:${runtimeConfig.server.port}`;
|
|
304
326
|
const res = openUrl(url);
|
|
@@ -323,7 +345,8 @@ function showHelp() {
|
|
|
323
345
|
|
|
324
346
|
Server commands:
|
|
325
347
|
serve [--dev] [--force] Start the server (--force allows a second instance)
|
|
326
|
-
setup
|
|
348
|
+
setup Choose providers and sign in (interactive)
|
|
349
|
+
login Sign in with ChatGPT for GPT OAuth (--device for headless)
|
|
327
350
|
status Show current configuration status
|
|
328
351
|
doctor Diagnose environment and setup
|
|
329
352
|
open Open web UI in browser
|
|
@@ -353,6 +376,7 @@ function showHelp() {
|
|
|
353
376
|
billing API usage / quota
|
|
354
377
|
providers Configured providers
|
|
355
378
|
oauth <sub> GPT OAuth proxy status (ima2 oauth --help)
|
|
379
|
+
gpt <sub> ChatGPT OAuth login/status/logout (ima2 gpt --help)
|
|
356
380
|
grok <sub> xAI OAuth login/status/logout (ima2 grok --help)
|
|
357
381
|
config <sub> Config get/set/ls/path/rm (ima2 config --help)
|
|
358
382
|
defaults <sub> Inspect/change model defaults (ima2 defaults --help)
|
|
@@ -424,7 +448,7 @@ if (args.includes("-v") || args.includes("--version")) {
|
|
|
424
448
|
console.log(pkg.version);
|
|
425
449
|
exitFlushed(0);
|
|
426
450
|
}
|
|
427
|
-
const helpOwningCommands = ["doctor", "gen", "video", "edit", "vectorize", "ls", "show", "ps", "cancel", "session", "history", "prompt", "multimode", "node", "annotate", "canvas-versions", "metadata", "comfy", "cardnews", "inflight", "storage", "billing", "providers", "oauth", "grok", "config", "defaults", "models", "capabilities", "tools", "skill", "ping", "backfill-thumbs", "service"];
|
|
451
|
+
const helpOwningCommands = ["doctor", "gen", "video", "edit", "vectorize", "ls", "show", "ps", "cancel", "session", "history", "prompt", "multimode", "node", "annotate", "canvas-versions", "metadata", "comfy", "cardnews", "inflight", "storage", "billing", "providers", "oauth", "grok", "gpt", "login", "config", "defaults", "models", "capabilities", "tools", "skill", "ping", "backfill-thumbs", "service"];
|
|
428
452
|
if (!command) {
|
|
429
453
|
showHelp();
|
|
430
454
|
exitFlushed(1);
|
|
@@ -449,15 +473,26 @@ switch (command) {
|
|
|
449
473
|
exitFlushed(Number(process.exitCode ?? 0));
|
|
450
474
|
break;
|
|
451
475
|
}
|
|
476
|
+
case "login": {
|
|
477
|
+
// 'ima2 login' signs in to ChatGPT now; it no longer rewrites the provider config.
|
|
478
|
+
const { default: gptCmd } = await import("./commands/gpt.js");
|
|
479
|
+
await gptCmd(["login", ...args.slice(1)]);
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
452
482
|
case "setup":
|
|
453
|
-
case "login":
|
|
454
483
|
setup().then(() => console.log(" Done. Run 'ima2 serve' to start.")).catch((e) => {
|
|
455
484
|
console.error(`Setup failed: ${e?.message || e}`);
|
|
456
485
|
exitFlushed(1);
|
|
457
486
|
});
|
|
458
487
|
break;
|
|
459
488
|
case "status":
|
|
460
|
-
|
|
489
|
+
if (args.includes("--json")) {
|
|
490
|
+
const report = await buildAuthReport();
|
|
491
|
+
console.log(JSON.stringify({ version: pkg.version, provider: loadConfig().provider ?? null, ...report }, null, 2));
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
await showStatus();
|
|
495
|
+
}
|
|
461
496
|
break;
|
|
462
497
|
case "doctor":
|
|
463
498
|
await doctor(args.slice(1));
|
|
@@ -512,6 +547,7 @@ switch (command) {
|
|
|
512
547
|
case "tools":
|
|
513
548
|
case "skill":
|
|
514
549
|
case "grok":
|
|
550
|
+
case "gpt":
|
|
515
551
|
case "ping": {
|
|
516
552
|
const { setCliVersion } = await import("./lib/client.js");
|
|
517
553
|
setCliVersion(pkg.version);
|
|
@@ -3,6 +3,7 @@ import { listProviders } from "../../lib/providers/registry.js";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { detectCodexAuth } from "../../lib/codexDetect.js";
|
|
6
|
+
import { gptAuthStatus } from "../../lib/authStatus.js";
|
|
6
7
|
import { config as runtimeConfig } from "../../config.js";
|
|
7
8
|
import { normalizeComfyOrigin } from "../../lib/comfyBridge.js";
|
|
8
9
|
function firstEnv(names) {
|
|
@@ -36,10 +37,24 @@ function inspectApiKey(lane, credential, fileConfig) {
|
|
|
36
37
|
}
|
|
37
38
|
function inspectOauth(lane) {
|
|
38
39
|
if (lane === "oauth") {
|
|
39
|
-
const
|
|
40
|
-
if (
|
|
41
|
-
return { code: "OAUTH_FILE_READY", lane, kind: "pass", text: `${lane}:
|
|
42
|
-
|
|
40
|
+
const status = gptAuthStatus();
|
|
41
|
+
if (status.health === "healthy") {
|
|
42
|
+
return { code: "OAUTH_FILE_READY", lane, kind: "pass", text: `${lane}: ChatGPT session ready (${status.source})` };
|
|
43
|
+
}
|
|
44
|
+
if (status.health === "warning") {
|
|
45
|
+
return { code: "OAUTH_FILE_READY", lane, kind: "warn", text: `${lane}: ChatGPT session shared with the Codex CLI; run ima2 login for an ima2-owned session` };
|
|
46
|
+
}
|
|
47
|
+
const keyringOnly = !status.loggedIn && status.reason === "no_session" && detectCodexAuth().probe === "authed";
|
|
48
|
+
return {
|
|
49
|
+
code: "OAUTH_FILE_REQUIRED",
|
|
50
|
+
lane,
|
|
51
|
+
kind: "fail",
|
|
52
|
+
text: keyringOnly
|
|
53
|
+
? `${lane}: Codex is keyring-only; run ima2 login`
|
|
54
|
+
: status.reason === "no_refresh_token"
|
|
55
|
+
? `${lane}: ChatGPT session has no refresh token; run ima2 login`
|
|
56
|
+
: `${lane}: no ChatGPT session; run ima2 login`,
|
|
57
|
+
};
|
|
43
58
|
}
|
|
44
59
|
if (lane === "grok") {
|
|
45
60
|
const home = homedir();
|
package/docs/API.md
CHANGED
|
@@ -151,7 +151,7 @@ inventing a runtime `lanes` availability result.
|
|
|
151
151
|
| `GET` | `/api/health` | Server health, version, paths, provider policy; includes `grok: { auth: "oauth" \| "none" }`, mirrored in `~/.ima2/server.json` |
|
|
152
152
|
| `POST` | `/api/admin/stop` | Clean shutdown (local admin only): requires the boot-generated `X-Ima2-Admin-Nonce` from `~/.ima2/server.json`; any request with an `Origin` header is refused (browser drive-by protection). Responds `202` then self-signals SIGTERM |
|
|
153
153
|
| `GET` | `/api/providers` | Provider availability and runtime ports |
|
|
154
|
-
| `GET` | `/api/oauth/status` | OAuth proxy status and visible models |
|
|
154
|
+
| `GET` | `/api/oauth/status` | OAuth proxy status and visible models, plus `auth` / `grokAuth` login verdicts: `{ provider, loggedIn, health: healthy\|warning\|reauth_required\|not_logged_in, reason?, source?, email?, plan?, accountId (masked), expiresAt?, refreshable, action?, note? }`. A proxy that exited because no session file exists reports `auth_required`. |
|
|
155
155
|
| `GET` | `/api/grok/status` | xAI OAuth session state and visible xAI image models: `ready`, `no_image_model`, `error`, or `offline` with `reason: "login_required"` when no session is stored |
|
|
156
156
|
| `GET` | `/api/billing` | Billing/status probe, including API key source when configured |
|
|
157
157
|
| `GET` | `/api/quota` | Provider quota: returns `{ codex, grok, nai }`. Codex windows are `5h` and `7d`. NovelAI returns a `v5-battery` remaining-charge window when available (`resetsAt` is the ISO ETA for +1%, not full recharge), with Anlas balances in `nai.anlasFixed` / `nai.anlasPurchased`; a missing meter has no window. NovelAI account email is always null. Eligible Grok Build xAI OIDC/external auth returns a `weekly` percentage/reset window from `GET /v1/billing?format=credits`. If unavailable, the legacy endpoint may return a `monthly` window plus `billing: { usedUsd, limitUsd }`. |
|
|
@@ -160,8 +160,10 @@ inventing a runtime `lanes` availability result.
|
|
|
160
160
|
|
|
161
161
|
| Method | Path | Notes |
|
|
162
162
|
|---|---|---|
|
|
163
|
-
| `POST` | `/api/auth/switch` | Start
|
|
163
|
+
| `POST` | `/api/auth/switch` | Start an OAuth login. Body: `{ "provider": "grok" \| "codex", "flow"?: "browser" \| "device" }`. `codex` defaults to the browser flow (callback on `localhost:1455`, so the browser must be on the server machine); `device` works from any machine. Grok always uses the device code. Returns `{ sessionId, flow, userCode, verificationUrl, expiresIn }`; `userCode` is empty for the browser flow. A completed `codex` login restarts the GPT OAuth proxy. |
|
|
164
164
|
| `GET` | `/api/auth/switch/:sessionId` | Poll switch-account session status. Returns `{ status }` where status is `pending`, `complete`, `error`, or `expired`. |
|
|
165
|
+
| `DELETE` | `/api/auth/switch/:sessionId` | Cancel a pending login and release its callback port. |
|
|
166
|
+
| `POST` | `/api/oauth/restart` | Respawn the GPT OAuth proxy so it reads the current session file (used by `ima2 login` / `ima2 gpt login` / `ima2 gpt logout`). Returns `{ restarted, reason? }`. |
|
|
165
167
|
|
|
166
168
|
The Switch Account flow opens a browser verification URL. Once the user completes the device-code step, the server saves the new credentials (Grok: `~/.progrok/auth.json`; Codex: via `codex login --device-auth`) and the session transitions to `complete`. This endpoint is surfaced as a **Switch Account** button in the Settings QuotaCard for Grok and Codex providers.
|
|
167
169
|
|
|
@@ -1053,7 +1055,8 @@ Most server routes under `/api/*` have a CLI wrapper. The exception is **Agent M
|
|
|
1053
1055
|
| `GET /api/storage/status` / `POST /api/storage/open-generated-dir` | `ima2 storage status` / `ima2 storage open` |
|
|
1054
1056
|
| `GET /api/billing` / `GET /api/providers` / `GET /api/oauth/status` / `GET /api/grok/status` | `ima2 billing` / `ima2 providers` / `ima2 oauth status` / `ima2 grok status` |
|
|
1055
1057
|
| `GET /api/quota` | Web UI only (Grok and NovelAI quota in Settings) |
|
|
1056
|
-
| `POST /api/auth/switch` / `GET /api/auth/switch/:sessionId` | Web UI
|
|
1058
|
+
| `POST /api/auth/switch` / `GET /api/auth/switch/:sessionId` | Web UI (Settings > QuotaCard > Switch Account) and `ima2 grok login` |
|
|
1059
|
+
| `POST /api/oauth/restart` | `ima2 login`, `ima2 gpt login`, `ima2 gpt logout` |
|
|
1057
1060
|
| `GET /api/health` | `ima2 ping` |
|
|
1058
1061
|
| `GET /api/capabilities` | `ima2 capabilities` |
|
|
1059
1062
|
| `GET /api/config/grok-planner` | — (Grok planner model query) |
|
|
@@ -4,7 +4,7 @@ Generated by `npm run test:inventory` (script: `scripts/classify-tests.mjs`).
|
|
|
4
4
|
|
|
5
5
|
_Tests considered "runtime-importing" if they import from `../lib/`, `../routes/`, `../bin/`, `../server`, or `../config`._
|
|
6
6
|
|
|
7
|
-
Total:
|
|
7
|
+
Total: 507 (runtime: 231, contract: 276)
|
|
8
8
|
|
|
9
9
|
## Runtime-importing tests
|
|
10
10
|
- `tests/advertise-payload-contract.test.ts`
|
|
@@ -37,6 +37,7 @@ Total: 504 (runtime: 228, contract: 276)
|
|
|
37
37
|
- `tests/assets-routes-contract.test.ts`
|
|
38
38
|
- `tests/assets-store-contract.test.ts`
|
|
39
39
|
- `tests/atlascloud-provider-contract.test.ts`
|
|
40
|
+
- `tests/auth-switch-routes.test.ts`
|
|
40
41
|
- `tests/backend-hardening.test.ts`
|
|
41
42
|
- `tests/backend-input-lan-hardening.test.ts`
|
|
42
43
|
- `tests/background-presets.test.ts`
|
|
@@ -46,6 +47,7 @@ Total: 504 (runtime: 228, contract: 276)
|
|
|
46
47
|
- `tests/capabilities-video-modes-contract.test.ts`
|
|
47
48
|
- `tests/card-news-contract.test.ts`
|
|
48
49
|
- `tests/card-news-template.test.ts`
|
|
50
|
+
- `tests/chatgpt-auth-contract.test.ts`
|
|
49
51
|
- `tests/classic-generate-async.test.ts`
|
|
50
52
|
- `tests/cli-error-hints.test.ts`
|
|
51
53
|
- `tests/cli-lan-auth.test.ts`
|
|
@@ -157,6 +159,7 @@ Total: 504 (runtime: 228, contract: 276)
|
|
|
157
159
|
- `tests/oauth-cli-dependencies.test.ts`
|
|
158
160
|
- `tests/oauth-normalize.test.ts`
|
|
159
161
|
- `tests/oauth-proxy-error-safety.test.ts`
|
|
162
|
+
- `tests/oauth-proxy-session-reload.test.ts`
|
|
160
163
|
- `tests/open-directory.test.ts`
|
|
161
164
|
- `tests/openai-execution-parity.test.ts`
|
|
162
165
|
- `tests/openai-transport-parity.test.ts`
|