traderclaw-cli 1.0.76 → 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.
@@ -2082,12 +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-terminal-box { background:#0a1f2e; border:1px solid #2a7a6a; border-radius:10px; padding:14px; margin-top:8px; }
2086
- .oauth-terminal-box ol { margin:8px 0 0 18px; padding:0; color:#c5d7f5; font-size:13px; line-height:1.55; }
2087
- .oauth-terminal-box li { margin-bottom:6px; }
2088
- .oauth-details { margin-top:14px; border:1px solid #2a3f6a; border-radius:10px; padding:10px 14px 14px; background:#0a1224; }
2089
- .oauth-details summary { list-style-position: outside; padding:4px 0; color:#b8cff5; font-weight:600; font-size:14px; }
2090
- .oauth-details[open] summary { margin-bottom:8px; }
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; }
2091
2092
  .ok-banner { color:#78f0a9; font-size:13px; margin-top:8px; }
2092
2093
  .err-banner { color:#ff6b6b; font-size:13px; margin-top:8px; }
2093
2094
  </style>
@@ -2136,52 +2137,28 @@ function wizardHtml(defaults) {
2136
2137
  </div>
2137
2138
  <div style="margin-top:12px;" id="llmOauthBlock" class="hidden">
2138
2139
  <p class="muted" style="margin-bottom:12px;">
2139
- <strong>Why doesn’t OpenClaw “just ask for a link” in the terminal?</strong> It does not need to. In the usual setup, you run OpenClaw’s login on the <strong>same machine</strong> where OpenClaw runs: ChatGPT sends your browser to <code>http://localhost:1455/…</code> and OpenClaw <strong>receives that callback automatically</strong> — no copying from the address bar. You only need to paste a long callback URL when your <strong>browser is on another computer</strong> than the one running OpenClaw (for example, you opened the sign-in page on your laptop but OpenClaw is on a VPS).
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.
2140
2141
  </p>
2141
- <div class="oauth-terminal-box">
2142
- <strong style="color:#8ef5d0;">Recommended: sign in from a terminal on this machine</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>
2143
2150
  <ol>
2144
- <li>Copy the command below (or type it).</li>
2145
- <li>Run it in a normal terminal on <strong>this</strong> host. Your browser may open; sign in with ChatGPT and approve access.</li>
2146
- <li>When the command finishes successfully, check <strong>“ChatGPT login is done”</strong> below — you do <strong>not</strong> need to paste a callback URL for this path.</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>
2147
2155
  </ol>
2148
- <div class="oauth-row" style="margin-top:12px;">
2149
- <input type="text" id="oauthTerminalCmdDisplay" readonly value="openclaw models auth login --provider openai-codex" style="flex:1 1 320px; font-size:13px;" />
2150
- <button type="button" id="oauthCopyTerminalCmdBtn" class="secondary">Copy command</button>
2156
+ <div class="oauth-actions">
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>
2151
2159
  </div>
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>
2152
2161
  </div>
2153
- <label style="display:flex; align-items:flex-start; gap:10px; font-size:14px; color:#e8eef9; margin-top:16px; cursor:pointer;">
2154
- <input id="llmOAuthSkipLogin" type="checkbox" style="width:auto; margin-top:3px;" />
2155
- <span><strong>ChatGPT login is done</strong> — I ran the command above in a terminal on <strong>this</strong> machine and OpenClaw completed without errors.</span>
2156
- </label>
2157
- <details id="oauthWizardAltDetails" class="oauth-details">
2158
- <summary>Alternative: no shell on this server — sign in using this browser only</summary>
2159
- <p class="muted" style="margin-top:8px;">
2160
- Use this when you <strong>cannot</strong> run a terminal on the machine where this wizard runs (typical remote VPS + browser on your laptop). You will copy the <strong>full URL from the address bar</strong> after ChatGPT redirects — even if the page shows “connection refused”, the URL in the bar is still valid.
2161
- </p>
2162
- <div class="oauth-flow" style="margin-top:10px;">
2163
- <strong style="color:#9ee6ff;">Steps</strong>
2164
- <ol>
2165
- <li>Click <strong>Get ChatGPT sign-in link</strong> — we run the same OpenClaw login process and show the <code>https://auth.openai.com/oauth/authorize?…</code> URL.</li>
2166
- <li>Open that URL, sign in, and let the browser redirect toward <code>localhost:1455</code>.</li>
2167
- <li>Paste the <strong>entire callback URL</strong> from the address bar into the box below, then click <strong>Submit to OpenClaw</strong>.</li>
2168
- </ol>
2169
- <div class="oauth-actions">
2170
- <button type="button" id="oauthGetLinkBtn" class="secondary">Get ChatGPT sign-in link</button>
2171
- <button type="button" id="oauthSubmitBtn" class="secondary" disabled>Submit to OpenClaw</button>
2172
- </div>
2173
- <div id="oauthUrlRow" class="oauth-row hidden">
2174
- <label style="flex:1 1 100%; font-size:12px; color:#9cb0de;">Sign-in URL (open in browser — not what you paste back)</label>
2175
- <input type="text" id="oauthUrlDisplay" readonly placeholder="Click “Get ChatGPT sign-in link” first" />
2176
- <button type="button" id="oauthCopyUrlBtn" class="secondary" disabled>Copy URL</button>
2177
- <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>
2178
- </div>
2179
- <p id="oauthFlowStatus" class="muted" style="margin-top:8px;" aria-live="polite"></p>
2180
- </div>
2181
- <label style="display:block; margin-top:14px;">Paste redirect URL or authorization code</label>
2182
- <textarea id="llmOAuthPaste" autocomplete="off" placeholder="Example: http://localhost:1455/auth/callback?code=… (full URL from address bar). You can paste only the code if that is all you have."></textarea>
2183
- </details>
2184
- <p class="muted" style="margin-top:12px;"><strong>SSH tip:</strong> To make your laptop’s browser hit OpenClaw on the server, you can run <code>ssh -L 1455:127.0.0.1:1455 user@this-server</code> before signing in so <code>localhost:1455</code> on your machine forwards to the wizard host.</p>
2185
2162
  </div>
2186
2163
  <p class="muted" id="llmLoadState" aria-live="polite">Loading LLM provider catalog...</p>
2187
2164
  <div id="llmLoadingHint" class="loading-hint" role="status" aria-live="polite">
@@ -2326,8 +2303,6 @@ function wizardHtml(defaults) {
2326
2303
  const llmOauthProviderNote = document.getElementById("llmOauthProviderNote");
2327
2304
  const llmApiKeyBlock = document.getElementById("llmApiKeyBlock");
2328
2305
  const llmOauthBlock = document.getElementById("llmOauthBlock");
2329
- const llmOAuthPasteEl = document.getElementById("llmOAuthPaste");
2330
- const llmOAuthSkipLoginEl = document.getElementById("llmOAuthSkipLogin");
2331
2306
  const telegramTokenEl = document.getElementById("telegramToken");
2332
2307
  const llmLoadStateEl = document.getElementById("llmLoadState");
2333
2308
  const llmLoadingHintEl = document.getElementById("llmLoadingHint");
@@ -2357,16 +2332,56 @@ function wizardHtml(defaults) {
2357
2332
  let savedApiKeyProvider = "";
2358
2333
  let oauthSessionId = null;
2359
2334
  let oauthWizardLoginDone = false;
2335
+ let oauthStartInFlight = false;
2336
+ let oauthPollTimer = null;
2337
+ let oauthOpenedInBrowser = false;
2338
+ let oauthFlowStartedAtMs = 0;
2360
2339
 
2361
- const oauthGetLinkBtn = document.getElementById("oauthGetLinkBtn");
2362
- const oauthSubmitBtn = document.getElementById("oauthSubmitBtn");
2363
- const oauthUrlRow = document.getElementById("oauthUrlRow");
2364
- const oauthUrlDisplay = document.getElementById("oauthUrlDisplay");
2365
- const oauthCopyUrlBtn = document.getElementById("oauthCopyUrlBtn");
2366
2340
  const oauthOpenUrlBtn = document.getElementById("oauthOpenUrlBtn");
2341
+ const oauthRetryBtn = document.getElementById("oauthRetryBtn");
2367
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
+ }
2368
2381
 
2369
2382
  async function cancelOauthSession() {
2383
+ stopOauthPolling();
2384
+ oauthStartInFlight = false;
2370
2385
  if (!oauthSessionId) {
2371
2386
  try {
2372
2387
  await fetch("/api/llm/oauth/cancel", { method: "POST", headers: { "content-type": "application/json" }, body: "{}" });
@@ -2388,17 +2403,19 @@ function wizardHtml(defaults) {
2388
2403
  }
2389
2404
 
2390
2405
  function resetOauthWizardState() {
2406
+ stopOauthPolling();
2391
2407
  oauthSessionId = null;
2392
2408
  oauthWizardLoginDone = false;
2393
- if (oauthUrlRow) oauthUrlRow.classList.add("hidden");
2394
- if (oauthUrlDisplay) oauthUrlDisplay.value = "";
2395
- if (oauthCopyUrlBtn) oauthCopyUrlBtn.disabled = true;
2396
- if (oauthOpenUrlBtn) {
2397
- oauthOpenUrlBtn.href = "#";
2398
- oauthOpenUrlBtn.setAttribute("aria-disabled", "true");
2399
- }
2400
- if (oauthSubmitBtn) oauthSubmitBtn.disabled = true;
2401
- 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.");
2402
2419
  }
2403
2420
 
2404
2421
  (function initLlmAuthDefaults() {
@@ -2407,10 +2424,6 @@ function wizardHtml(defaults) {
2407
2424
  llmAuthModeOauth.checked = true;
2408
2425
  llmAuthModeApiKey.checked = false;
2409
2426
  }
2410
- llmOAuthPasteEl.value = ${JSON.stringify(defaults.llmOAuthPaste || "")};
2411
- if (${defaults.llmOAuthSkipLogin === true ? "true" : "false"}) {
2412
- llmOAuthSkipLoginEl.checked = true;
2413
- }
2414
2427
  applyLlmAuthModeUi();
2415
2428
  })();
2416
2429
 
@@ -2431,6 +2444,9 @@ function wizardHtml(defaults) {
2431
2444
  llmOauthProviderNote.classList.remove("hidden");
2432
2445
  llmApiKeyBlock.classList.add("hidden");
2433
2446
  llmOauthBlock.classList.remove("hidden");
2447
+ if (!oauthWizardLoginDone && !oauthStartInFlight && !oauthSessionId) {
2448
+ void startOauthGuidedFlow();
2449
+ }
2434
2450
  } else {
2435
2451
  void cancelOauthSession();
2436
2452
  resetOauthWizardState();
@@ -2452,13 +2468,157 @@ function wizardHtml(defaults) {
2452
2468
  function hasRequiredInputs() {
2453
2469
  if (!llmCatalogReady || !Boolean(telegramTokenEl.value.trim())) return false;
2454
2470
  if (isOauthMode()) {
2455
- if (oauthWizardLoginDone) return true;
2456
- if (llmOAuthSkipLoginEl.checked) return true;
2457
- return Boolean(llmOAuthPasteEl.value.trim());
2471
+ return oauthWizardLoginDone === true;
2458
2472
  }
2459
2473
  return Boolean(llmProviderEl.value.trim()) && Boolean(llmCredentialEl.value.trim());
2460
2474
  }
2461
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
+
2462
2622
  /** All-or-nothing: 0 or 4 non-empty X fields; partial is invalid. */
2463
2623
  function xWizardFieldsStatus() {
2464
2624
  const fields = [
@@ -2635,8 +2795,8 @@ function wizardHtml(defaults) {
2635
2795
  llmProvider: oauth ? "openai-codex" : llmProviderEl.value.trim(),
2636
2796
  llmModel: llmModelEl.value.trim(),
2637
2797
  llmCredential: oauth ? "" : llmCredentialEl.value.trim(),
2638
- llmOAuthPaste: llmOAuthPasteEl.value.trim(),
2639
- llmOAuthSkipLogin: llmOAuthSkipLoginEl.checked,
2798
+ llmOAuthPaste: "",
2799
+ llmOAuthSkipLogin: oauth ? true : false,
2640
2800
  apiKey: document.getElementById("apiKey").value.trim(),
2641
2801
  telegramToken: document.getElementById("telegramToken").value.trim(),
2642
2802
  referralCode: document.getElementById("referralCode").value.trim(),
@@ -2645,16 +2805,12 @@ function wizardHtml(defaults) {
2645
2805
  xAccessTokenMain: xAccessTokenMainEl.value.trim(),
2646
2806
  xAccessTokenMainSecret: xAccessTokenMainSecretEl.value.trim(),
2647
2807
  };
2648
- if (oauth && oauthWizardLoginDone) {
2649
- payload.llmOAuthSkipLogin = true;
2650
- payload.llmOAuthPaste = "";
2651
- }
2652
2808
  if (oauth) {
2653
- if (!payload.llmOAuthSkipLogin && !payload.llmOAuthPaste && !oauthWizardLoginDone) {
2809
+ if (!oauthWizardLoginDone) {
2654
2810
  stateEl.textContent = "blocked";
2655
2811
  readyEl.textContent = "";
2656
2812
  manualEl.textContent =
2657
- "Codex OAuth: check “ChatGPT login is done” after running the terminal command on this machine, or expand “Alternative” and use Get link + paste + Submit, or finish wizard Submit if you already used those buttons.";
2813
+ "Codex OAuth is not completed yet. Use the guided sign-in above in this same browser and wait for the green success message.";
2658
2814
  return;
2659
2815
  }
2660
2816
  } else if (!payload.llmProvider || !payload.llmCredential) {
@@ -2905,150 +3061,23 @@ function wizardHtml(defaults) {
2905
3061
  llmAuthModeApiKey.addEventListener("change", onLlmAuthModeChange);
2906
3062
  llmAuthModeOauth.addEventListener("change", onLlmAuthModeChange);
2907
3063
  llmCredentialEl.addEventListener("input", updateStartButtonState);
2908
- llmOAuthPasteEl.addEventListener("input", updateStartButtonState);
2909
- llmOAuthSkipLoginEl.addEventListener("change", updateStartButtonState);
2910
-
2911
- if (oauthGetLinkBtn) {
2912
- oauthGetLinkBtn.addEventListener("click", async () => {
2913
- oauthWizardLoginDone = false;
2914
- if (oauthFlowStatus) {
2915
- oauthFlowStatus.className = "muted";
2916
- oauthFlowStatus.textContent = "Starting OpenClaw (same as running models auth login in a terminal)...";
2917
- }
2918
- oauthGetLinkBtn.disabled = true;
2919
- try {
2920
- const res = await fetch("/api/llm/oauth/start", { method: "POST" });
2921
- const data = await res.json().catch(() => ({}));
2922
- if (!res.ok) {
2923
- if (oauthFlowStatus) {
2924
- oauthFlowStatus.className = "err-banner";
2925
- let errText = data.message || data.error || "Could not get sign-in URL.";
2926
- if (data.detail) {
2927
- const d = String(data.detail).replace(/\s+/g, " ").trim();
2928
- errText += d ? " " + d.slice(0, 700) : "";
2929
- }
2930
- oauthFlowStatus.textContent = errText;
2931
- }
2932
- oauthGetLinkBtn.disabled = false;
2933
- return;
2934
- }
2935
- oauthSessionId = data.sessionId;
2936
- if (oauthUrlDisplay) oauthUrlDisplay.value = data.authUrl || "";
2937
- if (oauthUrlRow) oauthUrlRow.classList.remove("hidden");
2938
- if (oauthCopyUrlBtn) oauthCopyUrlBtn.disabled = !data.authUrl;
2939
- if (oauthOpenUrlBtn && data.authUrl) {
2940
- oauthOpenUrlBtn.href = data.authUrl;
2941
- oauthOpenUrlBtn.removeAttribute("aria-disabled");
2942
- }
2943
- if (oauthSubmitBtn) oauthSubmitBtn.disabled = false;
2944
- const altDetails = document.getElementById("oauthWizardAltDetails");
2945
- if (altDetails) altDetails.open = true;
2946
- if (oauthFlowStatus) {
2947
- oauthFlowStatus.className = "muted";
2948
- oauthFlowStatus.textContent =
2949
- "Open this URL in your browser. After ChatGPT redirects, copy the full callback URL from the address bar (localhost:1455?code=…) — even if the page says connection refused — then paste it below and click Submit.";
2950
- }
2951
- } catch (err) {
2952
- if (oauthFlowStatus) {
2953
- oauthFlowStatus.className = "err-banner";
2954
- oauthFlowStatus.textContent = err && err.message ? String(err.message) : "Request failed.";
2955
- }
2956
- }
2957
- oauthGetLinkBtn.disabled = false;
2958
- });
2959
- }
2960
-
2961
- if (oauthSubmitBtn) {
2962
- oauthSubmitBtn.addEventListener("click", async () => {
2963
- const paste = llmOAuthPasteEl.value.trim();
2964
- if (!paste) {
2965
- if (oauthFlowStatus) {
2966
- oauthFlowStatus.className = "err-banner";
2967
- oauthFlowStatus.textContent = "Paste the redirect URL (from the browser address bar) or the authorization code first.";
2968
- }
2969
- return;
2970
- }
2971
- if (!oauthSessionId) {
2972
- if (oauthFlowStatus) {
2973
- oauthFlowStatus.className = "err-banner";
2974
- oauthFlowStatus.textContent = "Click “Get ChatGPT sign-in link” first, or check “ChatGPT login is done” if you already signed in via the terminal command.";
2975
- }
2976
- return;
2977
- }
2978
- oauthSubmitBtn.disabled = true;
2979
- if (oauthFlowStatus) {
2980
- oauthFlowStatus.className = "muted";
2981
- oauthFlowStatus.textContent = "Sending to OpenClaw...";
2982
- }
2983
- try {
2984
- const res = await fetch("/api/llm/oauth/submit", {
2985
- method: "POST",
2986
- headers: { "content-type": "application/json" },
2987
- body: JSON.stringify({ sessionId: oauthSessionId, paste }),
2988
- });
2989
- const data = await res.json().catch(() => ({}));
2990
- if (!res.ok) {
2991
- if (oauthFlowStatus) {
2992
- oauthFlowStatus.className = "err-banner";
2993
- oauthFlowStatus.textContent = data.message || data.error || "Login failed.";
2994
- }
2995
- oauthSubmitBtn.disabled = false;
2996
- return;
2997
- }
2998
- oauthWizardLoginDone = true;
2999
- oauthSessionId = null;
3000
- llmOAuthSkipLoginEl.checked = true;
3001
- if (oauthFlowStatus) {
3002
- oauthFlowStatus.className = "ok-banner";
3003
- oauthFlowStatus.textContent = "OpenClaw saved your ChatGPT login. You can start installation below.";
3004
- }
3005
- oauthSubmitBtn.disabled = true;
3006
- updateStartButtonState();
3007
- } catch (err) {
3008
- if (oauthFlowStatus) {
3009
- oauthFlowStatus.className = "err-banner";
3010
- oauthFlowStatus.textContent = err && err.message ? String(err.message) : "Request failed.";
3011
- }
3012
- oauthSubmitBtn.disabled = false;
3013
- }
3014
- });
3015
- }
3016
-
3017
- if (oauthCopyUrlBtn && oauthUrlDisplay) {
3018
- oauthCopyUrlBtn.addEventListener("click", async () => {
3019
- const v = oauthUrlDisplay.value;
3020
- if (!v) return;
3021
- try {
3022
- await navigator.clipboard.writeText(v);
3023
- oauthCopyUrlBtn.textContent = "Copied";
3024
- setTimeout(() => {
3025
- oauthCopyUrlBtn.textContent = "Copy URL";
3026
- }, 1500);
3027
- } catch {
3028
- oauthCopyUrlBtn.textContent = "Copy failed";
3029
- setTimeout(() => {
3030
- oauthCopyUrlBtn.textContent = "Copy URL";
3031
- }, 1500);
3032
- }
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.");
3033
3072
  });
3034
3073
  }
3035
3074
 
3036
- const oauthTerminalCmdDisplay = document.getElementById("oauthTerminalCmdDisplay");
3037
- const oauthCopyTerminalCmdBtn = document.getElementById("oauthCopyTerminalCmdBtn");
3038
- if (oauthCopyTerminalCmdBtn && oauthTerminalCmdDisplay) {
3039
- oauthCopyTerminalCmdBtn.addEventListener("click", async () => {
3040
- const v = oauthTerminalCmdDisplay.value || "openclaw models auth login --provider openai-codex";
3041
- try {
3042
- await navigator.clipboard.writeText(v);
3043
- oauthCopyTerminalCmdBtn.textContent = "Copied";
3044
- setTimeout(() => {
3045
- oauthCopyTerminalCmdBtn.textContent = "Copy command";
3046
- }, 1500);
3047
- } catch {
3048
- oauthCopyTerminalCmdBtn.textContent = "Copy failed";
3049
- setTimeout(() => {
3050
- oauthCopyTerminalCmdBtn.textContent = "Copy command";
3051
- }, 1500);
3075
+ if (oauthRetryBtn) {
3076
+ oauthRetryBtn.addEventListener("click", async () => {
3077
+ await cancelOauthSession();
3078
+ resetOauthWizardState();
3079
+ if (isOauthMode()) {
3080
+ await startOauthGuidedFlow();
3052
3081
  }
3053
3082
  });
3054
3083
  }
@@ -3095,16 +3124,69 @@ async function cmdInstall(args) {
3095
3124
  let running = false;
3096
3125
  let shuttingDown = false;
3097
3126
 
3098
- /** 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. */
3099
3128
  const oauthSessions = new Map();
3100
3129
  const OPENAI_OAUTH_AUTHORIZE_RE = /https:\/\/auth\.openai\.com\/oauth\/authorize\S*/;
3101
3130
  const oauthSessionTtlMs = 15 * 60 * 1000;
3102
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
+
3103
3185
  function killOauthSession(sessionId, signal = "SIGTERM") {
3104
3186
  const s = oauthSessions.get(sessionId);
3105
3187
  if (!s) return;
3106
3188
  try {
3107
- s.child.kill(signal);
3189
+ if (s.child) s.child.kill(signal);
3108
3190
  } catch {
3109
3191
  /* ignore */
3110
3192
  }
@@ -3114,7 +3196,8 @@ async function cmdInstall(args) {
3114
3196
  function pruneExpiredOauthSessions() {
3115
3197
  const now = Date.now();
3116
3198
  for (const [id, s] of oauthSessions) {
3117
- if (now - s.createdAt > oauthSessionTtlMs) {
3199
+ const anchor = s.updatedAt || s.createdAt || now;
3200
+ if (now - anchor > oauthSessionTtlMs) {
3118
3201
  killOauthSession(id);
3119
3202
  }
3120
3203
  }
@@ -3183,6 +3266,35 @@ async function cmdInstall(args) {
3183
3266
  return;
3184
3267
  }
3185
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
+
3186
3298
  if (req.method === "POST" && req.url === "/api/llm/oauth/start") {
3187
3299
  pruneExpiredOauthSessions();
3188
3300
  if (running) {
@@ -3219,7 +3331,7 @@ async function cmdInstall(args) {
3219
3331
  ok: false,
3220
3332
  error: "oauth_url_timeout",
3221
3333
  message:
3222
- "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.",
3223
3335
  });
3224
3336
  }, 45_000);
3225
3337
 
@@ -3229,7 +3341,17 @@ async function cmdInstall(args) {
3229
3341
  if (!m || !m[0]) return;
3230
3342
  clearTimeout(urlTimeout);
3231
3343
  responded = true;
3232
- 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
+ });
3233
3355
  respondJson(200, { ok: true, sessionId, authUrl: m[0] });
3234
3356
  };
3235
3357
 
@@ -3258,14 +3380,25 @@ async function cmdInstall(args) {
3258
3380
  error: "oauth_login_exited_early",
3259
3381
  exitCode: code,
3260
3382
  message:
3261
- "OpenClaw exited before showing a sign-in URL. Try again; or run `openclaw models auth login --provider openai-codex` in a terminal on this machine and use the checkbox below.",
3383
+ "OpenClaw exited before showing a sign-in URL. Start OAuth again and keep the flow on this same machine/browser.",
3262
3384
  detail: stripAnsi(combined).slice(-4000),
3263
3385
  });
3264
3386
  return;
3265
3387
  }
3266
3388
  const pending = oauthSessions.get(sessionId);
3267
- if (pending && !pending.submitted) {
3268
- 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
+ }
3269
3402
  }
3270
3403
  });
3271
3404
  return;
@@ -3522,12 +3655,21 @@ async function cmdInstall(args) {
3522
3655
  server.listen(defaults.port, "127.0.0.1", resolve);
3523
3656
  });
3524
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
+
3525
3665
  const url = `http://127.0.0.1:${defaults.port}`;
3526
3666
  printSuccess(`Installer wizard is running at ${url}`);
3527
3667
  if (!openBrowser(url)) {
3528
3668
  printInfo(`Open this URL in your browser: ${url}`);
3529
3669
  }
3530
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>`);
3531
3673
  }
3532
3674
 
3533
3675
  async function cmdTestSession(args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "traderclaw-cli",
3
- "version": "1.0.76",
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.76"
20
+ "solana-traderclaw": "^1.0.77"
21
21
  },
22
22
  "keywords": [
23
23
  "traderclaw",