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.
Files changed (49) hide show
  1. package/bin/commands/doctor.js +13 -8
  2. package/bin/commands/gpt.js +216 -0
  3. package/bin/ima2.js +89 -53
  4. package/bin/lib/doctor-providers.js +19 -4
  5. package/docs/API.md +6 -3
  6. package/docs/migration/runtime-test-inventory.md +4 -1
  7. package/lib/authStatus.js +93 -0
  8. package/lib/chatgptAuth.js +236 -0
  9. package/lib/chatgptLogin.js +257 -0
  10. package/lib/codexDetect.js +16 -10
  11. package/lib/oauthLauncher.js +32 -0
  12. package/lib/oauthProxy/runtime.js +3 -0
  13. package/node_modules/openai-oauth/dist/chunk-2AENSHRT.js +84 -14
  14. package/node_modules/openai-oauth/package.json +2 -2
  15. package/package.json +3 -3
  16. package/routes/auth.js +111 -204
  17. package/routes/health.js +23 -9
  18. package/routes/quota.js +4 -9
  19. package/server.js +56 -11
  20. package/ui/dist/.vite/manifest.json +64 -64
  21. package/ui/dist/assets/{AgentWorkspace-CxbtxZYs.js → AgentWorkspace-C6ke6Eo4.js} +1 -1
  22. package/ui/dist/assets/{App-x_fzCXKb.js → App-pMN1OjNW.js} +8 -8
  23. package/ui/dist/assets/{AssetGenWorkspace-BIYh26wL.js → AssetGenWorkspace-DzEfpUy_.js} +2 -2
  24. package/ui/dist/assets/{AssetMediaLightbox-ordAqhjX.js → AssetMediaLightbox-Bx0azN0S.js} +1 -1
  25. package/ui/dist/assets/AssetsWorkspace-CHw1u9VC.js +1 -0
  26. package/ui/dist/assets/{CardNewsWorkspace-BfR-FMwo.js → CardNewsWorkspace-DsjqhaYO.js} +2 -2
  27. package/ui/dist/assets/{GenerationRequestLogPanel-CaVBCWhI.js → GenerationRequestLogPanel-EYEiHIr9.js} +1 -1
  28. package/ui/dist/assets/{HomeWorkspace-x-oJypd7.js → HomeWorkspace-DbnY1YF2.js} +1 -1
  29. package/ui/dist/assets/{KeyingPanel-SsAIeDy3.js → KeyingPanel-8J_RMrxg.js} +1 -1
  30. package/ui/dist/assets/{NodeCanvas-B1R-fOYq.js → NodeCanvas-DmjbNcTa.js} +1 -1
  31. package/ui/dist/assets/{PromptBuilderPanel-gGHOSb6l.js → PromptBuilderPanel-CL34Jpso.js} +1 -1
  32. package/ui/dist/assets/{PromptImportDialog-B5Wv2cie.js → PromptImportDialog-B24cvEkK.js} +2 -2
  33. package/ui/dist/assets/{PromptImportDiscoverySection-B5AdpMnV.js → PromptImportDiscoverySection-Bbw1_J_3.js} +1 -1
  34. package/ui/dist/assets/{PromptImportFolderSection-DgIWI0j2.js → PromptImportFolderSection-CMpxuE8e.js} +1 -1
  35. package/ui/dist/assets/PromptLibraryPanel-Ct98TJ61.js +2 -0
  36. package/ui/dist/assets/SettingsWorkspace-DL9Tm90P.js +1 -0
  37. package/ui/dist/assets/{SpriteRecipeWorkspace-ghZGUlM8.js → SpriteRecipeWorkspace-BJUH1ddP.js} +1 -1
  38. package/ui/dist/assets/{index-B-iNVdeL.js → index-CHeVGnoy.js} +6 -6
  39. package/ui/dist/assets/{index-Co3ZHk9P.js → index-D_LpLGWV.js} +3 -3
  40. package/ui/dist/assets/{pptxgen.es-D6s2Ip8Q.js → pptxgen.es-s4xfPXLT.js} +1 -1
  41. package/ui/dist/assets/{promptBuilderStore-CQxHJQz4.js → promptBuilderStore-DQR29LwN.js} +1 -1
  42. package/ui/dist/assets/useAgentDialogFocus-DAeS9IWG.js +1 -0
  43. package/ui/dist/index.html +1 -1
  44. package/vendor/openai-oauth-1.0.2-ima2.2.tgz +0 -0
  45. package/ui/dist/assets/AssetsWorkspace-BuiwpbQQ.js +0 -1
  46. package/ui/dist/assets/PromptLibraryPanel-CGcqRRsJ.js +0 -2
  47. package/ui/dist/assets/SettingsWorkspace-BhbAnrYw.js +0 -1
  48. package/ui/dist/assets/useAgentDialogFocus-ALfcs_Em.js +0 -1
  49. package/vendor/openai-oauth-1.0.2-ima2.1.tgz +0 -0
@@ -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 auth = detectCodexAuth();
191
- if (fileConfig.provider === "oauth" && !auth.proxyReady) {
192
- console.log(auth.authed
193
- ? " ✗ Codex is keyring-authenticated, but GPT OAuth needs a file-backed session; run 'ima2 login'"
194
- : " ✗ GPT OAuth has no file-backed Codex session; run 'ima2 login'");
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 (auth.proxyReady) {
198
- console.log(" ✓ GPT OAuth file-backed Codex session is ready");
199
- ok++;
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 { codexFileLoginArgs, detectCodexAuth } from "../lib/codexDetect.js";
13
- import { packageCliCommand } from "../lib/packageCli.js";
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
- function runCodexLogin() {
55
- const codex = packageCliCommand("@openai/codex", "codex", codexFileLoginArgs());
56
- execFileSync(codex.command, codex.args, { stdio: "inherit", windowsHide: true });
57
- if (!detectCodexAuth().proxyReady) {
58
- throw new Error("Codex login completed without a file-backed session for the GPT OAuth proxy");
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 auth = detectCodexAuth();
136
- if (!auth.proxyReady) {
137
- if (auth.authed) {
138
- console.log(" Codex is signed in through the OS keyring; ima2 needs a file-backed session.\n");
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
- runCodexLogin();
142
+ await runGptLogin();
143
143
  }
144
- catch {
145
- console.log("\n GPT login failed. Continuing with Grok...\n");
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
- const auth = detectCodexAuth();
170
- const hasAuth = auth.proxyReady;
171
- if (!hasAuth) {
172
- if (auth.authed) {
173
- console.log(" Codex is signed in through the OS keyring; ima2 needs a file-backed session.\n");
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
- runCodexLogin();
172
+ await runGptLogin();
178
173
  }
179
- catch {
180
- console.log("\n Login failed or cancelled. You can retry with 'ima2 serve'.\n");
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
- const how = auth.probe === "authed" ? "codex CLI" : "auth file";
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
- // Check OAuth auth files + codex CLI probe
282
- const auth = detectCodexAuth();
283
- console.log(` GPT OAuth sessions:`);
284
- console.log(` ${auth.files.codex} ${auth.fileHits.codex ? "✓" : "✗"}`);
285
- console.log(` ${auth.files.chatgpt} ${auth.fileHits.chatgpt ? "✓" : "✗"}`);
286
- if (auth.fileHits.xdgCodex) {
287
- console.log(` ${auth.files.xdgCodex} ✓`);
288
- }
289
- const probeLabel = auth.probe === "authed" ? "✓ authed"
290
- : auth.probe === "unauthed" ? "✗ not logged in"
291
- : auth.probe === "error" ? "✗ codex CLI failed"
292
- : "– codex CLI not found";
293
- console.log(` codex login status ${probeLabel}`);
294
- if (auth.authed && !auth.proxyReady) {
295
- console.log(" GPT OAuth proxy ✗ keyring-only; run 'ima2 login'");
296
- }
297
- else if (auth.proxyReady) {
298
- console.log(" GPT OAuth proxy ✓ file-backed session ready");
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, login Configure API key or GPT OAuth (interactive)
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
- showStatus();
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 auth = detectCodexAuth();
40
- if (auth.proxyReady)
41
- return { code: "OAUTH_FILE_READY", lane, kind: "pass", text: `${lane}: file-backed Codex session ready` };
42
- return { code: "OAUTH_FILE_REQUIRED", lane, kind: "fail", text: `${lane}: no file-backed Codex session; run ima2 login` };
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 a device-code OAuth flow. Body: `{ "provider": "grok" \| "codex" }`. Returns `{ sessionId, userCode, verificationUrl }`. |
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 only (Settings > QuotaCard > Switch Account) |
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: 504 (runtime: 228, contract: 276)
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`