traderclaw-cli 1.0.75 → 1.0.77

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.
@@ -1594,16 +1594,38 @@ function configureOpenClawLlmModelPrimaryOnly({ provider, model }, configPath =
1594
1594
  return { configPath, provider, model };
1595
1595
  }
1596
1596
 
1597
+ /**
1598
+ * Spawns `openclaw models auth login --provider openai-codex` with a pseudo-TTY when possible.
1599
+ * The CLI often exits immediately when stdin/stdout are plain pipes (no TTY). On Unix, `script(1)`
1600
+ * allocates a PTY so the same flow works as in an interactive terminal.
1601
+ */
1602
+ export function spawnOpenClawCodexAuthLoginChild() {
1603
+ const argv = ["models", "auth", "login", "--provider", "openai-codex"];
1604
+ if (process.platform === "win32") {
1605
+ return spawn("openclaw", argv, { stdio: ["pipe", "pipe", "pipe"], shell: false });
1606
+ }
1607
+ // `unbuffer` (expect package) runs the CLI under a PTY and forwards stdin for the paste step reliably.
1608
+ // Plain `script` often does not forward Node's stdin to the inner openclaw process, which causes hangs until timeout.
1609
+ if (commandExists("unbuffer")) {
1610
+ return spawn("unbuffer", ["openclaw", ...argv], { stdio: ["pipe", "pipe", "pipe"], shell: false });
1611
+ }
1612
+ if (commandExists("script")) {
1613
+ const cmdline = "openclaw models auth login --provider openai-codex";
1614
+ return spawn("script", ["-q", "-c", cmdline, "/dev/null"], {
1615
+ stdio: ["pipe", "pipe", "pipe"],
1616
+ shell: false,
1617
+ });
1618
+ }
1619
+ return spawn("openclaw", argv, { stdio: ["pipe", "pipe", "pipe"], shell: false });
1620
+ }
1621
+
1597
1622
  /**
1598
1623
  * Runs `openclaw models auth login --provider openai-codex` and feeds the pasted redirect URL or code on stdin
1599
1624
  * when the CLI prompts (with a timed fallback for non-interactive / SSH).
1600
1625
  */
1601
1626
  function runOpenClawCodexOAuthLogin(paste, emitLog) {
1602
1627
  return new Promise((resolve, reject) => {
1603
- const child = spawn("openclaw", ["models", "auth", "login", "--provider", "openai-codex"], {
1604
- stdio: ["pipe", "pipe", "pipe"],
1605
- shell: false,
1606
- });
1628
+ const child = spawnOpenClawCodexAuthLoginChild();
1607
1629
 
1608
1630
  let stdout = "";
1609
1631
  let stderr = "";
@@ -6,7 +6,7 @@ import { dirname, join } from "path";
6
6
  import { fileURLToPath, pathToFileURL } from "url";
7
7
  import { homedir } from "os";
8
8
  import { randomUUID, createPrivateKey, sign as cryptoSign } from "crypto";
9
- import { execFile, execSync, spawn } from "child_process";
9
+ import { execFile, execSync } from "child_process";
10
10
  import { promisify } from "util";
11
11
  import { createServer } from "http";
12
12
  import { resolvePluginPackageRoot } from "./resolve-plugin-root.mjs";
@@ -2082,6 +2082,13 @@ function wizardHtml(defaults) {
2082
2082
  .oauth-row input[readonly] { flex:1 1 280px; font-size:12px; }
2083
2083
  .oauth-actions { display:flex; gap:8px; flex-wrap:wrap; margin-top:10px; }
2084
2084
  .oauth-actions button.secondary { background:#334a87; }
2085
+ .oauth-guide { background:#0a1f2e; border:1px solid #2a7a6a; border-radius:10px; padding:14px; margin-top:8px; }
2086
+ .oauth-guide ol { margin:10px 0 0 18px; padding:0; color:#d9e8ff; font-size:13px; line-height:1.55; }
2087
+ .oauth-guide li { margin-bottom:7px; }
2088
+ .oauth-step.pending { color:#9cb0de; }
2089
+ .oauth-step.active { color:#9ee6ff; font-weight:700; }
2090
+ .oauth-step.done { color:#78f0a9; }
2091
+ .oauth-step.error { color:#ff9b9b; font-weight:700; }
2085
2092
  .ok-banner { color:#78f0a9; font-size:13px; margin-top:8px; }
2086
2093
  .err-banner { color:#ff6b6b; font-size:13px; margin-top:8px; }
2087
2094
  </style>
@@ -2129,36 +2136,29 @@ function wizardHtml(defaults) {
2129
2136
  <p class="muted">Written to OpenClaw <code>config.env</code> for the selected provider. If you do not choose a model manually, the installer picks a safe default.</p>
2130
2137
  </div>
2131
2138
  <div style="margin-top:12px;" id="llmOauthBlock" class="hidden">
2132
- <p class="muted" style="margin-bottom:10px;">
2133
- Follow the flow below to sign in with ChatGPT and submit your login to OpenClaw.
2139
+ <p class="muted" style="margin-bottom:12px;">
2140
+ OAuth here is fully guided. After you choose OAuth, we start OpenClaw login automatically. Use the sign-in button below, then complete ChatGPT in this browser. You should not need to copy any code.
2134
2141
  </p>
2135
- <div class="oauth-flow">
2136
- <strong style="color:#9ee6ff;">Guided sign-in (recommended)</strong>
2142
+ <div class="oauth-guide">
2143
+ <p class="muted" style="margin-top:0;">
2144
+ <strong>Important:</strong> Complete the ChatGPT sign-in in this browser, then return to this tab. The wizard detects the result automatically.
2145
+ </p>
2146
+ <p class="muted" style="margin-top:4px;font-size:13px;color:#7a8ba8;">
2147
+ <strong>Remote VPS?</strong> Forward port <code>1455</code> alongside the wizard port:
2148
+ <code style="display:block;margin-top:4px;padding:4px 8px;background:#111827;border-radius:4px;font-size:12px;">ssh -L 17890:127.0.0.1:17890 -L 1455:127.0.0.1:1455 user@your-vps</code>
2149
+ </p>
2137
2150
  <ol>
2138
- <li>Click <strong>Get ChatGPT sign-in link</strong> — we run the same command as OpenClaw and show the long <code>https://auth.openai.com/oauth/authorize?…</code> URL here.</li>
2139
- <li>Open that URL in your <strong>local</strong> browser (on your PC if this is SSH), sign in with ChatGPT.</li>
2140
- <li>After login, copy either the <strong>full URL</strong> from the address bar or the <strong>code</strong> from the prompt, and paste into the box below.</li>
2141
- <li>Click <strong>Submit to OpenClaw</strong>. When it succeeds, you can start installation — no need to guess what to paste.</li>
2151
+ <li class="oauth-step pending" id="oauthStepPrepare">Preparing ChatGPT sign-in...</li>
2152
+ <li class="oauth-step pending" id="oauthStepOpen">Open ChatGPT sign-in in this browser.</li>
2153
+ <li class="oauth-step pending" id="oauthStepComplete">Finish ChatGPT approval, then return here.</li>
2154
+ <li class="oauth-step pending" id="oauthStepVerify">We detect completion automatically.</li>
2142
2155
  </ol>
2143
2156
  <div class="oauth-actions">
2144
- <button type="button" id="oauthGetLinkBtn" class="secondary">1. Get ChatGPT sign-in link</button>
2145
- <button type="button" id="oauthSubmitBtn" class="secondary" disabled>3. Submit to OpenClaw</button>
2157
+ <a id="oauthOpenUrlBtn" class="secondary" href="#" target="_blank" rel="noopener noreferrer" style="display:inline-block;padding:10px 14px;border-radius:8px;background:#2d7dff;color:#fff;text-decoration:none;font-weight:600;opacity:.55;pointer-events:none;">Open ChatGPT sign-in</a>
2158
+ <button type="button" id="oauthRetryBtn" class="secondary hidden">Try sign-in again</button>
2146
2159
  </div>
2147
- <div id="oauthUrlRow" class="oauth-row hidden">
2148
- <label style="flex:1 1 100%; font-size:12px; color:#9cb0de;">Sign-in URL (open this in your browser — this is not what you paste back)</label>
2149
- <input type="text" id="oauthUrlDisplay" readonly placeholder="Click “Get ChatGPT sign-in link” first" />
2150
- <button type="button" id="oauthCopyUrlBtn" class="secondary" disabled>Copy URL</button>
2151
- <a id="oauthOpenUrlBtn" class="secondary" href="#" target="_blank" rel="noopener noreferrer" style="display:inline-block;padding:10px 14px;border-radius:8px;background:#2d7dff;color:#fff;text-decoration:none;font-weight:600;">Open in browser</a>
2152
- </div>
2153
- <p id="oauthFlowStatus" class="muted" style="margin-top:8px;" aria-live="polite"></p>
2160
+ <p id="oauthFlowStatus" class="muted" style="margin-top:8px;" aria-live="polite">Choose OAuth and wait a moment. We will prepare your sign-in automatically.</p>
2154
2161
  </div>
2155
- <label style="margin-top:14px;">Paste redirect URL or authorization code (step 2 → 3)</label>
2156
- <textarea id="llmOAuthPaste" autocomplete="off" placeholder="After signing in at ChatGPT, paste here: the full http://127.0.0.1:1455/... or http://localhost:1455/... URL from your browser, OR the code string if the prompt asks for the code only."></textarea>
2157
- <label style="display:flex; align-items:flex-start; gap:8px; font-size:13px; color:#9cb0de; margin-top:8px; cursor:pointer;">
2158
- <input id="llmOAuthSkipLogin" type="checkbox" style="width:auto; margin-top:3px;" />
2159
- <span>I already completed <code>openclaw models auth login --provider openai-codex</code> in a terminal on <strong>this</strong> machine (skip the buttons above)</span>
2160
- </label>
2161
- <p class="muted">SSH tip: the browser must reach your machine for localhost callbacks; if that fails, run the same login command in a desktop terminal on this host, then use the checkbox.</p>
2162
2162
  </div>
2163
2163
  <p class="muted" id="llmLoadState" aria-live="polite">Loading LLM provider catalog...</p>
2164
2164
  <div id="llmLoadingHint" class="loading-hint" role="status" aria-live="polite">
@@ -2303,8 +2303,6 @@ function wizardHtml(defaults) {
2303
2303
  const llmOauthProviderNote = document.getElementById("llmOauthProviderNote");
2304
2304
  const llmApiKeyBlock = document.getElementById("llmApiKeyBlock");
2305
2305
  const llmOauthBlock = document.getElementById("llmOauthBlock");
2306
- const llmOAuthPasteEl = document.getElementById("llmOAuthPaste");
2307
- const llmOAuthSkipLoginEl = document.getElementById("llmOAuthSkipLogin");
2308
2306
  const telegramTokenEl = document.getElementById("telegramToken");
2309
2307
  const llmLoadStateEl = document.getElementById("llmLoadState");
2310
2308
  const llmLoadingHintEl = document.getElementById("llmLoadingHint");
@@ -2334,16 +2332,56 @@ function wizardHtml(defaults) {
2334
2332
  let savedApiKeyProvider = "";
2335
2333
  let oauthSessionId = null;
2336
2334
  let oauthWizardLoginDone = false;
2335
+ let oauthStartInFlight = false;
2336
+ let oauthPollTimer = null;
2337
+ let oauthOpenedInBrowser = false;
2338
+ let oauthFlowStartedAtMs = 0;
2337
2339
 
2338
- const oauthGetLinkBtn = document.getElementById("oauthGetLinkBtn");
2339
- const oauthSubmitBtn = document.getElementById("oauthSubmitBtn");
2340
- const oauthUrlRow = document.getElementById("oauthUrlRow");
2341
- const oauthUrlDisplay = document.getElementById("oauthUrlDisplay");
2342
- const oauthCopyUrlBtn = document.getElementById("oauthCopyUrlBtn");
2343
2340
  const oauthOpenUrlBtn = document.getElementById("oauthOpenUrlBtn");
2341
+ const oauthRetryBtn = document.getElementById("oauthRetryBtn");
2344
2342
  const oauthFlowStatus = document.getElementById("oauthFlowStatus");
2343
+ const oauthStepPrepare = document.getElementById("oauthStepPrepare");
2344
+ const oauthStepOpen = document.getElementById("oauthStepOpen");
2345
+ const oauthStepComplete = document.getElementById("oauthStepComplete");
2346
+ const oauthStepVerify = document.getElementById("oauthStepVerify");
2347
+
2348
+ function setOauthStep(stepEl, mode) {
2349
+ if (!stepEl) return;
2350
+ stepEl.classList.remove("pending", "active", "done", "error");
2351
+ stepEl.classList.add(mode || "pending");
2352
+ }
2353
+
2354
+ function setOauthStatus(text, isError = false, isSuccess = false) {
2355
+ if (!oauthFlowStatus) return;
2356
+ oauthFlowStatus.className = isError ? "err-banner" : isSuccess ? "ok-banner" : "muted";
2357
+ oauthFlowStatus.textContent = text;
2358
+ }
2359
+
2360
+ function setOauthOpenButton(url) {
2361
+ if (!oauthOpenUrlBtn) return;
2362
+ if (!url) {
2363
+ oauthOpenUrlBtn.href = "#";
2364
+ oauthOpenUrlBtn.style.opacity = ".55";
2365
+ oauthOpenUrlBtn.style.pointerEvents = "none";
2366
+ oauthOpenUrlBtn.setAttribute("aria-disabled", "true");
2367
+ return;
2368
+ }
2369
+ oauthOpenUrlBtn.href = url;
2370
+ oauthOpenUrlBtn.style.opacity = "1";
2371
+ oauthOpenUrlBtn.style.pointerEvents = "auto";
2372
+ oauthOpenUrlBtn.removeAttribute("aria-disabled");
2373
+ }
2374
+
2375
+ function stopOauthPolling() {
2376
+ if (oauthPollTimer) {
2377
+ clearInterval(oauthPollTimer);
2378
+ oauthPollTimer = null;
2379
+ }
2380
+ }
2345
2381
 
2346
2382
  async function cancelOauthSession() {
2383
+ stopOauthPolling();
2384
+ oauthStartInFlight = false;
2347
2385
  if (!oauthSessionId) {
2348
2386
  try {
2349
2387
  await fetch("/api/llm/oauth/cancel", { method: "POST", headers: { "content-type": "application/json" }, body: "{}" });
@@ -2365,17 +2403,19 @@ function wizardHtml(defaults) {
2365
2403
  }
2366
2404
 
2367
2405
  function resetOauthWizardState() {
2406
+ stopOauthPolling();
2368
2407
  oauthSessionId = null;
2369
2408
  oauthWizardLoginDone = false;
2370
- if (oauthUrlRow) oauthUrlRow.classList.add("hidden");
2371
- if (oauthUrlDisplay) oauthUrlDisplay.value = "";
2372
- if (oauthCopyUrlBtn) oauthCopyUrlBtn.disabled = true;
2373
- if (oauthOpenUrlBtn) {
2374
- oauthOpenUrlBtn.href = "#";
2375
- oauthOpenUrlBtn.setAttribute("aria-disabled", "true");
2376
- }
2377
- if (oauthSubmitBtn) oauthSubmitBtn.disabled = true;
2378
- if (oauthFlowStatus) oauthFlowStatus.textContent = "";
2409
+ oauthStartInFlight = false;
2410
+ oauthOpenedInBrowser = false;
2411
+ oauthFlowStartedAtMs = 0;
2412
+ if (oauthRetryBtn) oauthRetryBtn.classList.add("hidden");
2413
+ setOauthOpenButton("");
2414
+ setOauthStep(oauthStepPrepare, "pending");
2415
+ setOauthStep(oauthStepOpen, "pending");
2416
+ setOauthStep(oauthStepComplete, "pending");
2417
+ setOauthStep(oauthStepVerify, "pending");
2418
+ setOauthStatus("Choose OAuth and wait a moment. We will prepare your sign-in automatically.");
2379
2419
  }
2380
2420
 
2381
2421
  (function initLlmAuthDefaults() {
@@ -2384,10 +2424,6 @@ function wizardHtml(defaults) {
2384
2424
  llmAuthModeOauth.checked = true;
2385
2425
  llmAuthModeApiKey.checked = false;
2386
2426
  }
2387
- llmOAuthPasteEl.value = ${JSON.stringify(defaults.llmOAuthPaste || "")};
2388
- if (${defaults.llmOAuthSkipLogin === true ? "true" : "false"}) {
2389
- llmOAuthSkipLoginEl.checked = true;
2390
- }
2391
2427
  applyLlmAuthModeUi();
2392
2428
  })();
2393
2429
 
@@ -2408,6 +2444,9 @@ function wizardHtml(defaults) {
2408
2444
  llmOauthProviderNote.classList.remove("hidden");
2409
2445
  llmApiKeyBlock.classList.add("hidden");
2410
2446
  llmOauthBlock.classList.remove("hidden");
2447
+ if (!oauthWizardLoginDone && !oauthStartInFlight && !oauthSessionId) {
2448
+ void startOauthGuidedFlow();
2449
+ }
2411
2450
  } else {
2412
2451
  void cancelOauthSession();
2413
2452
  resetOauthWizardState();
@@ -2429,13 +2468,157 @@ function wizardHtml(defaults) {
2429
2468
  function hasRequiredInputs() {
2430
2469
  if (!llmCatalogReady || !Boolean(telegramTokenEl.value.trim())) return false;
2431
2470
  if (isOauthMode()) {
2432
- if (oauthWizardLoginDone) return true;
2433
- if (llmOAuthSkipLoginEl.checked) return true;
2434
- return Boolean(llmOAuthPasteEl.value.trim());
2471
+ return oauthWizardLoginDone === true;
2435
2472
  }
2436
2473
  return Boolean(llmProviderEl.value.trim()) && Boolean(llmCredentialEl.value.trim());
2437
2474
  }
2438
2475
 
2476
+ async function pollOauthStatusOnce() {
2477
+ if (!oauthSessionId || !isOauthMode()) return;
2478
+ try {
2479
+ const sessionId = oauthSessionId;
2480
+ const res = await fetch("/api/llm/oauth/status?sessionId=" + encodeURIComponent(sessionId));
2481
+ const data = await res.json().catch(() => ({}));
2482
+ if (!res.ok) {
2483
+ stopOauthPolling();
2484
+ oauthStartInFlight = false;
2485
+ if (data && data.error === "invalid_or_expired_session") {
2486
+ setOauthStep(oauthStepPrepare, "done");
2487
+ setOauthStep(oauthStepOpen, "error");
2488
+ setOauthStep(oauthStepComplete, "error");
2489
+ setOauthStep(oauthStepVerify, "error");
2490
+ setOauthStatus(
2491
+ "Automatic OAuth session expired. This flow requires the wizard and OpenClaw on the same machine and the same browser. Click Try sign-in again.",
2492
+ true,
2493
+ );
2494
+ } else {
2495
+ setOauthStatus((data && (data.message || data.error)) || "Could not read OAuth status.", true);
2496
+ }
2497
+ if (oauthRetryBtn) oauthRetryBtn.classList.remove("hidden");
2498
+ updateStartButtonState();
2499
+ return;
2500
+ }
2501
+ const state = typeof data.state === "string" ? data.state : "unknown";
2502
+ if (state === "awaiting_browser_callback") {
2503
+ setOauthStep(oauthStepPrepare, "done");
2504
+ setOauthStep(oauthStepOpen, oauthOpenedInBrowser ? "done" : "active");
2505
+ setOauthStep(oauthStepComplete, oauthOpenedInBrowser ? "active" : "pending");
2506
+ setOauthStep(oauthStepVerify, oauthOpenedInBrowser ? "active" : "pending");
2507
+ if (oauthOpenedInBrowser) {
2508
+ const elapsed = oauthFlowStartedAtMs > 0 ? Math.floor((Date.now() - oauthFlowStartedAtMs) / 1000) : 0;
2509
+ if (elapsed >= 90) {
2510
+ setOauthStatus(
2511
+ "Still waiting for callback. Keep using this same browser on this same machine. If you opened OAuth on another computer, this automatic flow cannot complete.",
2512
+ true,
2513
+ );
2514
+ if (oauthRetryBtn) oauthRetryBtn.classList.remove("hidden");
2515
+ } else {
2516
+ setOauthStatus("Waiting for ChatGPT approval to finish. Return here after you continue.", false);
2517
+ }
2518
+ } else {
2519
+ setOauthStatus("Step 2: click Open ChatGPT sign-in, then complete approval and return here.", false);
2520
+ }
2521
+ updateStartButtonState();
2522
+ return;
2523
+ }
2524
+ stopOauthPolling();
2525
+ oauthStartInFlight = false;
2526
+ if (state === "succeeded") {
2527
+ oauthWizardLoginDone = true;
2528
+ oauthSessionId = null;
2529
+ setOauthStep(oauthStepPrepare, "done");
2530
+ setOauthStep(oauthStepOpen, "done");
2531
+ setOauthStep(oauthStepComplete, "done");
2532
+ setOauthStep(oauthStepVerify, "done");
2533
+ setOauthStatus(data.message || "ChatGPT is connected. You can start installation now.", false, true);
2534
+ setOauthOpenButton("");
2535
+ if (oauthRetryBtn) oauthRetryBtn.classList.add("hidden");
2536
+ updateStartButtonState();
2537
+ return;
2538
+ }
2539
+ const errorText = data.message || data.error || "OAuth login failed.";
2540
+ setOauthStep(oauthStepPrepare, "done");
2541
+ setOauthStep(oauthStepOpen, "error");
2542
+ setOauthStep(oauthStepComplete, "error");
2543
+ setOauthStep(oauthStepVerify, "error");
2544
+ setOauthStatus(
2545
+ errorText + " Automatic mode only works on the same machine and browser. Click Try sign-in again.",
2546
+ true,
2547
+ );
2548
+ setOauthOpenButton("");
2549
+ if (oauthRetryBtn) oauthRetryBtn.classList.remove("hidden");
2550
+ updateStartButtonState();
2551
+ } catch (err) {
2552
+ stopOauthPolling();
2553
+ oauthStartInFlight = false;
2554
+ setOauthStep(oauthStepPrepare, "done");
2555
+ setOauthStep(oauthStepOpen, "error");
2556
+ setOauthStep(oauthStepComplete, "error");
2557
+ setOauthStep(oauthStepVerify, "error");
2558
+ setOauthStatus(err && err.message ? String(err.message) : "OAuth status request failed.", true);
2559
+ if (oauthRetryBtn) oauthRetryBtn.classList.remove("hidden");
2560
+ updateStartButtonState();
2561
+ }
2562
+ }
2563
+
2564
+ function beginOauthStatusPolling() {
2565
+ stopOauthPolling();
2566
+ oauthPollTimer = setInterval(() => {
2567
+ void pollOauthStatusOnce();
2568
+ }, 1500);
2569
+ void pollOauthStatusOnce();
2570
+ }
2571
+
2572
+ async function startOauthGuidedFlow() {
2573
+ if (!isOauthMode() || oauthStartInFlight) return;
2574
+ oauthStartInFlight = true;
2575
+ oauthWizardLoginDone = false;
2576
+ oauthOpenedInBrowser = false;
2577
+ oauthFlowStartedAtMs = Date.now();
2578
+ if (oauthRetryBtn) oauthRetryBtn.classList.add("hidden");
2579
+ setOauthStep(oauthStepPrepare, "active");
2580
+ setOauthStep(oauthStepOpen, "pending");
2581
+ setOauthStep(oauthStepComplete, "pending");
2582
+ setOauthStep(oauthStepVerify, "pending");
2583
+ setOauthStatus("Preparing ChatGPT sign-in link...");
2584
+ setOauthOpenButton("");
2585
+ updateStartButtonState();
2586
+ try {
2587
+ const res = await fetch("/api/llm/oauth/start", { method: "POST" });
2588
+ const data = await res.json().catch(() => ({}));
2589
+ if (!res.ok) {
2590
+ oauthStartInFlight = false;
2591
+ setOauthStep(oauthStepPrepare, "error");
2592
+ setOauthStep(oauthStepOpen, "error");
2593
+ setOauthStep(oauthStepComplete, "error");
2594
+ setOauthStep(oauthStepVerify, "error");
2595
+ setOauthStatus((data && (data.message || data.error)) || "Could not start OAuth sign-in.", true);
2596
+ if (oauthRetryBtn) oauthRetryBtn.classList.remove("hidden");
2597
+ updateStartButtonState();
2598
+ return;
2599
+ }
2600
+ oauthSessionId = typeof data.sessionId === "string" ? data.sessionId : "";
2601
+ const authUrl = typeof data.authUrl === "string" ? data.authUrl : "";
2602
+ if (!oauthSessionId || !authUrl) {
2603
+ oauthStartInFlight = false;
2604
+ setOauthStep(oauthStepPrepare, "error");
2605
+ setOauthStatus("OpenClaw did not return a valid sign-in URL. Try again.", true);
2606
+ if (oauthRetryBtn) oauthRetryBtn.classList.remove("hidden");
2607
+ return;
2608
+ }
2609
+ setOauthOpenButton(authUrl);
2610
+ setOauthStep(oauthStepPrepare, "done");
2611
+ setOauthStep(oauthStepOpen, "active");
2612
+ setOauthStatus("Step 2: click Open ChatGPT sign-in and continue in this same browser.");
2613
+ beginOauthStatusPolling();
2614
+ } catch (err) {
2615
+ oauthStartInFlight = false;
2616
+ setOauthStep(oauthStepPrepare, "error");
2617
+ setOauthStatus(err && err.message ? String(err.message) : "Could not start OAuth sign-in.", true);
2618
+ if (oauthRetryBtn) oauthRetryBtn.classList.remove("hidden");
2619
+ }
2620
+ }
2621
+
2439
2622
  /** All-or-nothing: 0 or 4 non-empty X fields; partial is invalid. */
2440
2623
  function xWizardFieldsStatus() {
2441
2624
  const fields = [
@@ -2612,8 +2795,8 @@ function wizardHtml(defaults) {
2612
2795
  llmProvider: oauth ? "openai-codex" : llmProviderEl.value.trim(),
2613
2796
  llmModel: llmModelEl.value.trim(),
2614
2797
  llmCredential: oauth ? "" : llmCredentialEl.value.trim(),
2615
- llmOAuthPaste: llmOAuthPasteEl.value.trim(),
2616
- llmOAuthSkipLogin: llmOAuthSkipLoginEl.checked,
2798
+ llmOAuthPaste: "",
2799
+ llmOAuthSkipLogin: oauth ? true : false,
2617
2800
  apiKey: document.getElementById("apiKey").value.trim(),
2618
2801
  telegramToken: document.getElementById("telegramToken").value.trim(),
2619
2802
  referralCode: document.getElementById("referralCode").value.trim(),
@@ -2622,16 +2805,12 @@ function wizardHtml(defaults) {
2622
2805
  xAccessTokenMain: xAccessTokenMainEl.value.trim(),
2623
2806
  xAccessTokenMainSecret: xAccessTokenMainSecretEl.value.trim(),
2624
2807
  };
2625
- if (oauth && oauthWizardLoginDone) {
2626
- payload.llmOAuthSkipLogin = true;
2627
- payload.llmOAuthPaste = "";
2628
- }
2629
2808
  if (oauth) {
2630
- if (!payload.llmOAuthSkipLogin && !payload.llmOAuthPaste && !oauthWizardLoginDone) {
2809
+ if (!oauthWizardLoginDone) {
2631
2810
  stateEl.textContent = "blocked";
2632
2811
  readyEl.textContent = "";
2633
2812
  manualEl.textContent =
2634
- "Codex OAuth: use “Get ChatGPT sign-in link” and “Submit to OpenClaw”, paste a redirect URL/code, or check the box if you already ran openclaw models auth login on this machine.";
2813
+ "Codex OAuth is not completed yet. Use the guided sign-in above in this same browser and wait for the green success message.";
2635
2814
  return;
2636
2815
  }
2637
2816
  } else if (!payload.llmProvider || !payload.llmCredential) {
@@ -2882,123 +3061,23 @@ function wizardHtml(defaults) {
2882
3061
  llmAuthModeApiKey.addEventListener("change", onLlmAuthModeChange);
2883
3062
  llmAuthModeOauth.addEventListener("change", onLlmAuthModeChange);
2884
3063
  llmCredentialEl.addEventListener("input", updateStartButtonState);
2885
- llmOAuthPasteEl.addEventListener("input", updateStartButtonState);
2886
- llmOAuthSkipLoginEl.addEventListener("change", updateStartButtonState);
2887
-
2888
- if (oauthGetLinkBtn) {
2889
- oauthGetLinkBtn.addEventListener("click", async () => {
2890
- oauthWizardLoginDone = false;
2891
- if (oauthFlowStatus) {
2892
- oauthFlowStatus.className = "muted";
2893
- oauthFlowStatus.textContent = "Starting OpenClaw (same as running models auth login in a terminal)...";
2894
- }
2895
- oauthGetLinkBtn.disabled = true;
2896
- try {
2897
- const res = await fetch("/api/llm/oauth/start", { method: "POST" });
2898
- const data = await res.json().catch(() => ({}));
2899
- if (!res.ok) {
2900
- if (oauthFlowStatus) {
2901
- oauthFlowStatus.className = "err-banner";
2902
- oauthFlowStatus.textContent = data.message || data.error || "Could not get sign-in URL.";
2903
- }
2904
- oauthGetLinkBtn.disabled = false;
2905
- return;
2906
- }
2907
- oauthSessionId = data.sessionId;
2908
- if (oauthUrlDisplay) oauthUrlDisplay.value = data.authUrl || "";
2909
- if (oauthUrlRow) oauthUrlRow.classList.remove("hidden");
2910
- if (oauthCopyUrlBtn) oauthCopyUrlBtn.disabled = !data.authUrl;
2911
- if (oauthOpenUrlBtn && data.authUrl) {
2912
- oauthOpenUrlBtn.href = data.authUrl;
2913
- oauthOpenUrlBtn.removeAttribute("aria-disabled");
2914
- }
2915
- if (oauthSubmitBtn) oauthSubmitBtn.disabled = false;
2916
- if (oauthFlowStatus) {
2917
- oauthFlowStatus.className = "muted";
2918
- oauthFlowStatus.textContent =
2919
- "Open this URL in your browser (on your PC if you use SSH). After ChatGPT sign-in, paste the localhost redirect URL or the code, then click Submit.";
2920
- }
2921
- } catch (err) {
2922
- if (oauthFlowStatus) {
2923
- oauthFlowStatus.className = "err-banner";
2924
- oauthFlowStatus.textContent = err && err.message ? String(err.message) : "Request failed.";
2925
- }
2926
- }
2927
- oauthGetLinkBtn.disabled = false;
2928
- });
2929
- }
2930
-
2931
- if (oauthSubmitBtn) {
2932
- oauthSubmitBtn.addEventListener("click", async () => {
2933
- const paste = llmOAuthPasteEl.value.trim();
2934
- if (!paste) {
2935
- if (oauthFlowStatus) {
2936
- oauthFlowStatus.className = "err-banner";
2937
- oauthFlowStatus.textContent = "Paste the redirect URL (from the browser address bar) or the authorization code first.";
2938
- }
2939
- return;
2940
- }
2941
- if (!oauthSessionId) {
2942
- if (oauthFlowStatus) {
2943
- oauthFlowStatus.className = "err-banner";
2944
- oauthFlowStatus.textContent = "Click “Get ChatGPT sign-in link” first, or use the “already completed login in terminal” checkbox.";
2945
- }
2946
- return;
2947
- }
2948
- oauthSubmitBtn.disabled = true;
2949
- if (oauthFlowStatus) {
2950
- oauthFlowStatus.className = "muted";
2951
- oauthFlowStatus.textContent = "Sending to OpenClaw...";
2952
- }
2953
- try {
2954
- const res = await fetch("/api/llm/oauth/submit", {
2955
- method: "POST",
2956
- headers: { "content-type": "application/json" },
2957
- body: JSON.stringify({ sessionId: oauthSessionId, paste }),
2958
- });
2959
- const data = await res.json().catch(() => ({}));
2960
- if (!res.ok) {
2961
- if (oauthFlowStatus) {
2962
- oauthFlowStatus.className = "err-banner";
2963
- oauthFlowStatus.textContent = data.message || data.error || "Login failed.";
2964
- }
2965
- oauthSubmitBtn.disabled = false;
2966
- return;
2967
- }
2968
- oauthWizardLoginDone = true;
2969
- oauthSessionId = null;
2970
- llmOAuthSkipLoginEl.checked = true;
2971
- if (oauthFlowStatus) {
2972
- oauthFlowStatus.className = "ok-banner";
2973
- oauthFlowStatus.textContent = "OpenClaw saved your ChatGPT login. You can start installation below.";
2974
- }
2975
- oauthSubmitBtn.disabled = true;
2976
- updateStartButtonState();
2977
- } catch (err) {
2978
- if (oauthFlowStatus) {
2979
- oauthFlowStatus.className = "err-banner";
2980
- oauthFlowStatus.textContent = err && err.message ? String(err.message) : "Request failed.";
2981
- }
2982
- oauthSubmitBtn.disabled = false;
2983
- }
3064
+ if (oauthOpenUrlBtn) {
3065
+ oauthOpenUrlBtn.addEventListener("click", () => {
3066
+ if (!oauthSessionId || oauthWizardLoginDone) return;
3067
+ oauthOpenedInBrowser = true;
3068
+ setOauthStep(oauthStepOpen, "done");
3069
+ setOauthStep(oauthStepComplete, "active");
3070
+ setOauthStep(oauthStepVerify, "active");
3071
+ setOauthStatus("Complete ChatGPT approval in this same browser, then return here. We detect completion automatically.");
2984
3072
  });
2985
3073
  }
2986
3074
 
2987
- if (oauthCopyUrlBtn && oauthUrlDisplay) {
2988
- oauthCopyUrlBtn.addEventListener("click", async () => {
2989
- const v = oauthUrlDisplay.value;
2990
- if (!v) return;
2991
- try {
2992
- await navigator.clipboard.writeText(v);
2993
- oauthCopyUrlBtn.textContent = "Copied";
2994
- setTimeout(() => {
2995
- oauthCopyUrlBtn.textContent = "Copy URL";
2996
- }, 1500);
2997
- } catch {
2998
- oauthCopyUrlBtn.textContent = "Copy failed";
2999
- setTimeout(() => {
3000
- oauthCopyUrlBtn.textContent = "Copy URL";
3001
- }, 1500);
3075
+ if (oauthRetryBtn) {
3076
+ oauthRetryBtn.addEventListener("click", async () => {
3077
+ await cancelOauthSession();
3078
+ resetOauthWizardState();
3079
+ if (isOauthMode()) {
3080
+ await startOauthGuidedFlow();
3002
3081
  }
3003
3082
  });
3004
3083
  }
@@ -3024,7 +3103,8 @@ async function cmdInstall(args) {
3024
3103
  }
3025
3104
 
3026
3105
  const defaults = parseInstallWizardArgs(args);
3027
- const { createInstallerStepEngine, assertWizardXCredentials } = await import("./installer-step-engine.mjs");
3106
+ const { createInstallerStepEngine, assertWizardXCredentials, spawnOpenClawCodexAuthLoginChild } =
3107
+ await import("./installer-step-engine.mjs");
3028
3108
  const modeConfig = {
3029
3109
  pluginPackage: "solana-traderclaw",
3030
3110
  pluginId: "solana-trader",
@@ -3044,16 +3124,69 @@ async function cmdInstall(args) {
3044
3124
  let running = false;
3045
3125
  let shuttingDown = false;
3046
3126
 
3047
- /** In-browser Codex OAuth: one pending `openclaw models auth login` child waiting for stdin (same flow as CLI). */
3127
+ /** Guided Codex OAuth sessions keyed by sessionId. */
3048
3128
  const oauthSessions = new Map();
3049
3129
  const OPENAI_OAUTH_AUTHORIZE_RE = /https:\/\/auth\.openai\.com\/oauth\/authorize\S*/;
3050
3130
  const oauthSessionTtlMs = 15 * 60 * 1000;
3051
3131
 
3132
+ // Long-lived callback proxy on port 1455. Bound at wizard startup so
3133
+ // Cursor/VSCode auto-forwards it to the user's laptop before they begin
3134
+ // the OAuth flow. When ChatGPT redirects the browser to localhost:1455,
3135
+ // this proxy catches the callback and feeds the code to the active
3136
+ // openclaw child process.
3137
+ let oauthCallbackProxy = null;
3138
+
3139
+ function findActiveOauthSession() {
3140
+ for (const [, s] of oauthSessions) {
3141
+ if (s.status === "awaiting_browser_callback" && s.child) return s;
3142
+ }
3143
+ return null;
3144
+ }
3145
+
3146
+ function startCallbackProxy() {
3147
+ if (oauthCallbackProxy) return;
3148
+ try {
3149
+ oauthCallbackProxy = createServer((cbReq, cbRes) => {
3150
+ const s = findActiveOauthSession();
3151
+ const fullUrl = `http://localhost:1455${cbReq.url}`;
3152
+ if (s && cbReq.url && cbReq.url.includes("code=")) {
3153
+ s.updatedAt = Date.now();
3154
+ try {
3155
+ if (s.child && s.child.stdin && !s.child.stdin.writableEnded && !s.child.stdin.destroyed) {
3156
+ s.child.stdin.write(`${fullUrl}\n`, () => {
3157
+ setTimeout(() => {
3158
+ try {
3159
+ if (s.child && s.child.stdin && !s.child.stdin.destroyed && !s.child.stdin.writableEnded) {
3160
+ s.child.stdin.end();
3161
+ }
3162
+ } catch { /* ignore */ }
3163
+ }, 100);
3164
+ });
3165
+ }
3166
+ } catch { /* ignore */ }
3167
+ cbRes.statusCode = 200;
3168
+ cbRes.setHeader("content-type", "text/html; charset=utf-8");
3169
+ cbRes.end(`<html><body style="background:#0a0e1a;color:#78f0a9;font-family:system-ui;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;flex-direction:column;"><h2>ChatGPT sign-in received!</h2><p style="color:#c5d7f5;font-size:16px;">You can close this tab and return to the installer wizard.</p></body></html>`);
3170
+ } else {
3171
+ cbRes.statusCode = 200;
3172
+ cbRes.setHeader("content-type", "text/html; charset=utf-8");
3173
+ cbRes.end(`<html><body style="background:#0a0e1a;color:#9cb0de;font-family:system-ui;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;flex-direction:column;"><h2>TraderClaw OAuth Callback Listener</h2><p>Waiting for ChatGPT redirect. If you see this page, the callback proxy is working.</p></body></html>`);
3174
+ }
3175
+ });
3176
+ oauthCallbackProxy.listen(1455, "127.0.0.1");
3177
+ oauthCallbackProxy.on("error", () => {
3178
+ oauthCallbackProxy = null;
3179
+ });
3180
+ } catch {
3181
+ oauthCallbackProxy = null;
3182
+ }
3183
+ }
3184
+
3052
3185
  function killOauthSession(sessionId, signal = "SIGTERM") {
3053
3186
  const s = oauthSessions.get(sessionId);
3054
3187
  if (!s) return;
3055
3188
  try {
3056
- s.child.kill(signal);
3189
+ if (s.child) s.child.kill(signal);
3057
3190
  } catch {
3058
3191
  /* ignore */
3059
3192
  }
@@ -3063,7 +3196,8 @@ async function cmdInstall(args) {
3063
3196
  function pruneExpiredOauthSessions() {
3064
3197
  const now = Date.now();
3065
3198
  for (const [id, s] of oauthSessions) {
3066
- if (now - s.createdAt > oauthSessionTtlMs) {
3199
+ const anchor = s.updatedAt || s.createdAt || now;
3200
+ if (now - anchor > oauthSessionTtlMs) {
3067
3201
  killOauthSession(id);
3068
3202
  }
3069
3203
  }
@@ -3132,6 +3266,35 @@ async function cmdInstall(args) {
3132
3266
  return;
3133
3267
  }
3134
3268
 
3269
+ if (req.method === "GET" && req.url.startsWith("/api/llm/oauth/status")) {
3270
+ pruneExpiredOauthSessions();
3271
+ let sessionId = "";
3272
+ try {
3273
+ const u = new URL(req.url, "http://127.0.0.1");
3274
+ sessionId = String(u.searchParams.get("sessionId") || "").trim();
3275
+ } catch {
3276
+ sessionId = "";
3277
+ }
3278
+ if (!sessionId) {
3279
+ respondJson(400, { ok: false, error: "session_id_required" });
3280
+ return;
3281
+ }
3282
+ const s = oauthSessions.get(sessionId);
3283
+ if (!s) {
3284
+ respondJson(404, { ok: false, error: "invalid_or_expired_session", message: "OAuth session expired. Start sign-in again." });
3285
+ return;
3286
+ }
3287
+ respondJson(200, {
3288
+ ok: true,
3289
+ state: s.status || "unknown",
3290
+ message: s.message || "",
3291
+ authUrl: s.authUrl || "",
3292
+ exitCode: typeof s.exitCode === "number" ? s.exitCode : null,
3293
+ detail: s.detail || "",
3294
+ });
3295
+ return;
3296
+ }
3297
+
3135
3298
  if (req.method === "POST" && req.url === "/api/llm/oauth/start") {
3136
3299
  pruneExpiredOauthSessions();
3137
3300
  if (running) {
@@ -3151,10 +3314,7 @@ async function cmdInstall(args) {
3151
3314
  }
3152
3315
 
3153
3316
  const sessionId = randomUUID();
3154
- const child = spawn("openclaw", ["models", "auth", "login", "--provider", "openai-codex"], {
3155
- stdio: ["pipe", "pipe", "pipe"],
3156
- shell: false,
3157
- });
3317
+ const child = spawnOpenClawCodexAuthLoginChild();
3158
3318
 
3159
3319
  let combined = "";
3160
3320
  let responded = false;
@@ -3171,7 +3331,7 @@ async function cmdInstall(args) {
3171
3331
  ok: false,
3172
3332
  error: "oauth_url_timeout",
3173
3333
  message:
3174
- "OpenClaw did not print a ChatGPT sign-in URL in time. Run `openclaw models auth login --provider openai-codex` in a terminal on this machine, then use the checkbox below.",
3334
+ "OpenClaw did not provide a ChatGPT sign-in URL in time. Try again.",
3175
3335
  });
3176
3336
  }, 45_000);
3177
3337
 
@@ -3181,7 +3341,17 @@ async function cmdInstall(args) {
3181
3341
  if (!m || !m[0]) return;
3182
3342
  clearTimeout(urlTimeout);
3183
3343
  responded = true;
3184
- oauthSessions.set(sessionId, { child, createdAt: Date.now(), submitted: false });
3344
+ oauthSessions.set(sessionId, {
3345
+ child,
3346
+ createdAt: Date.now(),
3347
+ updatedAt: Date.now(),
3348
+ status: "awaiting_browser_callback",
3349
+ authUrl: m[0],
3350
+ message: "Sign in in this same browser. OpenClaw is waiting for callback...",
3351
+ detail: "",
3352
+ exitCode: null,
3353
+ submitted: false,
3354
+ });
3185
3355
  respondJson(200, { ok: true, sessionId, authUrl: m[0] });
3186
3356
  };
3187
3357
 
@@ -3209,13 +3379,26 @@ async function cmdInstall(args) {
3209
3379
  ok: false,
3210
3380
  error: "oauth_login_exited_early",
3211
3381
  exitCode: code,
3212
- detail: combined.slice(-4000),
3382
+ message:
3383
+ "OpenClaw exited before showing a sign-in URL. Start OAuth again and keep the flow on this same machine/browser.",
3384
+ detail: stripAnsi(combined).slice(-4000),
3213
3385
  });
3214
3386
  return;
3215
3387
  }
3216
3388
  const pending = oauthSessions.get(sessionId);
3217
- if (pending && !pending.submitted) {
3218
- oauthSessions.delete(sessionId);
3389
+ if (pending) {
3390
+ pending.child = null;
3391
+ pending.updatedAt = Date.now();
3392
+ pending.exitCode = typeof code === "number" ? code : null;
3393
+ pending.detail = stripAnsi(combined).slice(-4000);
3394
+ if (code === 0) {
3395
+ pending.status = "succeeded";
3396
+ pending.message = "ChatGPT OAuth completed successfully.";
3397
+ } else {
3398
+ pending.status = "failed";
3399
+ pending.message =
3400
+ "OpenClaw OAuth did not complete. Try again — make sure you continue in the same browser that opened the sign-in link.";
3401
+ }
3219
3402
  }
3220
3403
  });
3221
3404
  return;
@@ -3256,7 +3439,7 @@ async function cmdInstall(args) {
3256
3439
  error: "oauth_submit_timeout",
3257
3440
  message: "Login did not finish in time. Try again or complete login in a normal terminal.",
3258
3441
  });
3259
- }, 120_000);
3442
+ }, 300_000);
3260
3443
 
3261
3444
  child.once("close", (code) => {
3262
3445
  if (code === 0) {
@@ -3272,7 +3455,29 @@ async function cmdInstall(args) {
3272
3455
  });
3273
3456
 
3274
3457
  try {
3275
- child.stdin.write(`${paste}\n`);
3458
+ const line = `${paste}\n`;
3459
+ if (child.stdin?.writableEnded || child.stdin?.destroyed) {
3460
+ killOauthSession(sessionId);
3461
+ finish(500, { ok: false, error: "stdin_closed", message: "The login session closed before paste could be sent." });
3462
+ return;
3463
+ }
3464
+ child.stdin.write(line, (err) => {
3465
+ if (err) {
3466
+ killOauthSession(sessionId);
3467
+ finish(500, { ok: false, error: "stdin_write_failed", message: err?.message || String(err) });
3468
+ return;
3469
+ }
3470
+ // End stdin so the PTY/readline layer forwards the line and the CLI can proceed (plain `script` often buffers otherwise).
3471
+ setTimeout(() => {
3472
+ try {
3473
+ if (child.stdin && !child.stdin.destroyed && !child.stdin.writableEnded) {
3474
+ child.stdin.end();
3475
+ }
3476
+ } catch {
3477
+ /* ignore */
3478
+ }
3479
+ }, 100);
3480
+ });
3276
3481
  } catch (err) {
3277
3482
  killOauthSession(sessionId);
3278
3483
  finish(500, { ok: false, error: "stdin_write_failed", message: err?.message || String(err) });
@@ -3450,12 +3655,21 @@ async function cmdInstall(args) {
3450
3655
  server.listen(defaults.port, "127.0.0.1", resolve);
3451
3656
  });
3452
3657
 
3658
+ // Start the OAuth callback proxy on port 1455 early so Cursor/VSCode
3659
+ // auto-forwards it to the user's laptop before the OAuth flow begins.
3660
+ startCallbackProxy();
3661
+ if (oauthCallbackProxy) {
3662
+ printInfo(`OAuth callback proxy listening on http://127.0.0.1:1455`);
3663
+ }
3664
+
3453
3665
  const url = `http://127.0.0.1:${defaults.port}`;
3454
3666
  printSuccess(`Installer wizard is running at ${url}`);
3455
3667
  if (!openBrowser(url)) {
3456
3668
  printInfo(`Open this URL in your browser: ${url}`);
3457
3669
  }
3458
3670
  printInfo("Press Ctrl+C to stop the wizard server.");
3671
+ printInfo(`If you are on a remote VPS, forward both ports from your local machine:`);
3672
+ printInfo(` ssh -L ${defaults.port}:127.0.0.1:${defaults.port} -L 1455:127.0.0.1:1455 <user>@<your-vps>`);
3459
3673
  }
3460
3674
 
3461
3675
  async function cmdTestSession(args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "traderclaw-cli",
3
- "version": "1.0.75",
3
+ "version": "1.0.77",
4
4
  "description": "Global TraderClaw CLI (install --wizard, setup, precheck). Installs solana-traderclaw as a dependency for OpenClaw plugin files.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,7 +17,7 @@
17
17
  "node": ">=22"
18
18
  },
19
19
  "dependencies": {
20
- "solana-traderclaw": "^1.0.75"
20
+ "solana-traderclaw": "^1.0.77"
21
21
  },
22
22
  "keywords": [
23
23
  "traderclaw",