traderclaw-cli 1.0.76 → 1.0.78

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,38 @@ 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>
2152
- </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>
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>
2161
+ <div id="oauthFallbackPaste" class="hidden" style="margin-top:12px;padding:12px;background:#111827;border:1px solid #334a87;border-radius:8px;">
2162
+ <p class="muted" style="margin:0 0 8px;font-size:13px;color:#ffcc70;">
2163
+ <strong>Redirect didn't reach us?</strong> Copy the full URL from the error page in your browser (it starts with <code>http://localhost:1455/auth/callback?code=…</code>) and paste it below.
2164
+ </p>
2165
+ <div style="display:flex;gap:8px;">
2166
+ <input id="oauthFallbackUrlInput" type="text" placeholder="Paste the full localhost:1455/auth/callback?code=… URL here" style="flex:1;font-size:12px;padding:8px 10px;border-radius:6px;border:1px solid #334a87;background:#0a1224;color:#e0e8ff;">
2167
+ <button type="button" id="oauthFallbackSubmitBtn" class="secondary" style="padding:8px 14px;white-space:nowrap;">Submit URL</button>
2178
2168
  </div>
2179
- <p id="oauthFlowStatus" class="muted" style="margin-top:8px;" aria-live="polite"></p>
2169
+ <p id="oauthFallbackError" class="muted hidden" style="margin:6px 0 0;font-size:12px;color:#ff6b6b;"></p>
2180
2170
  </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>
2171
+ </div>
2185
2172
  </div>
2186
2173
  <p class="muted" id="llmLoadState" aria-live="polite">Loading LLM provider catalog...</p>
2187
2174
  <div id="llmLoadingHint" class="loading-hint" role="status" aria-live="polite">
@@ -2326,8 +2313,6 @@ function wizardHtml(defaults) {
2326
2313
  const llmOauthProviderNote = document.getElementById("llmOauthProviderNote");
2327
2314
  const llmApiKeyBlock = document.getElementById("llmApiKeyBlock");
2328
2315
  const llmOauthBlock = document.getElementById("llmOauthBlock");
2329
- const llmOAuthPasteEl = document.getElementById("llmOAuthPaste");
2330
- const llmOAuthSkipLoginEl = document.getElementById("llmOAuthSkipLogin");
2331
2316
  const telegramTokenEl = document.getElementById("telegramToken");
2332
2317
  const llmLoadStateEl = document.getElementById("llmLoadState");
2333
2318
  const llmLoadingHintEl = document.getElementById("llmLoadingHint");
@@ -2357,16 +2342,61 @@ function wizardHtml(defaults) {
2357
2342
  let savedApiKeyProvider = "";
2358
2343
  let oauthSessionId = null;
2359
2344
  let oauthWizardLoginDone = false;
2345
+ let oauthStartInFlight = false;
2346
+ let oauthPollTimer = null;
2347
+ let oauthOpenedInBrowser = false;
2348
+ let oauthFlowStartedAtMs = 0;
2360
2349
 
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
2350
  const oauthOpenUrlBtn = document.getElementById("oauthOpenUrlBtn");
2351
+ const oauthRetryBtn = document.getElementById("oauthRetryBtn");
2367
2352
  const oauthFlowStatus = document.getElementById("oauthFlowStatus");
2353
+ const oauthStepPrepare = document.getElementById("oauthStepPrepare");
2354
+ const oauthStepOpen = document.getElementById("oauthStepOpen");
2355
+ const oauthStepComplete = document.getElementById("oauthStepComplete");
2356
+ const oauthStepVerify = document.getElementById("oauthStepVerify");
2357
+ const oauthFallbackPaste = document.getElementById("oauthFallbackPaste");
2358
+ const oauthFallbackUrlInput = document.getElementById("oauthFallbackUrlInput");
2359
+ const oauthFallbackSubmitBtn = document.getElementById("oauthFallbackSubmitBtn");
2360
+ const oauthFallbackError = document.getElementById("oauthFallbackError");
2361
+ let oauthFallbackTimer = null;
2362
+
2363
+ function setOauthStep(stepEl, mode) {
2364
+ if (!stepEl) return;
2365
+ stepEl.classList.remove("pending", "active", "done", "error");
2366
+ stepEl.classList.add(mode || "pending");
2367
+ }
2368
+
2369
+ function setOauthStatus(text, isError = false, isSuccess = false) {
2370
+ if (!oauthFlowStatus) return;
2371
+ oauthFlowStatus.className = isError ? "err-banner" : isSuccess ? "ok-banner" : "muted";
2372
+ oauthFlowStatus.textContent = text;
2373
+ }
2374
+
2375
+ function setOauthOpenButton(url) {
2376
+ if (!oauthOpenUrlBtn) return;
2377
+ if (!url) {
2378
+ oauthOpenUrlBtn.href = "#";
2379
+ oauthOpenUrlBtn.style.opacity = ".55";
2380
+ oauthOpenUrlBtn.style.pointerEvents = "none";
2381
+ oauthOpenUrlBtn.setAttribute("aria-disabled", "true");
2382
+ return;
2383
+ }
2384
+ oauthOpenUrlBtn.href = url;
2385
+ oauthOpenUrlBtn.style.opacity = "1";
2386
+ oauthOpenUrlBtn.style.pointerEvents = "auto";
2387
+ oauthOpenUrlBtn.removeAttribute("aria-disabled");
2388
+ }
2389
+
2390
+ function stopOauthPolling() {
2391
+ if (oauthPollTimer) {
2392
+ clearInterval(oauthPollTimer);
2393
+ oauthPollTimer = null;
2394
+ }
2395
+ }
2368
2396
 
2369
2397
  async function cancelOauthSession() {
2398
+ stopOauthPolling();
2399
+ oauthStartInFlight = false;
2370
2400
  if (!oauthSessionId) {
2371
2401
  try {
2372
2402
  await fetch("/api/llm/oauth/cancel", { method: "POST", headers: { "content-type": "application/json" }, body: "{}" });
@@ -2387,18 +2417,37 @@ function wizardHtml(defaults) {
2387
2417
  oauthSessionId = null;
2388
2418
  }
2389
2419
 
2420
+ function hideFallbackPaste() {
2421
+ if (oauthFallbackPaste) oauthFallbackPaste.classList.add("hidden");
2422
+ if (oauthFallbackUrlInput) oauthFallbackUrlInput.value = "";
2423
+ if (oauthFallbackError) { oauthFallbackError.textContent = ""; oauthFallbackError.classList.add("hidden"); }
2424
+ if (oauthFallbackTimer) { clearTimeout(oauthFallbackTimer); oauthFallbackTimer = null; }
2425
+ }
2426
+
2427
+ function showFallbackPasteAfterDelay(ms) {
2428
+ hideFallbackPaste();
2429
+ oauthFallbackTimer = setTimeout(() => {
2430
+ if (oauthFallbackPaste && oauthSessionId && oauthOpenedInBrowser && !oauthWizardLoginDone) {
2431
+ oauthFallbackPaste.classList.remove("hidden");
2432
+ }
2433
+ }, ms);
2434
+ }
2435
+
2390
2436
  function resetOauthWizardState() {
2437
+ stopOauthPolling();
2438
+ hideFallbackPaste();
2391
2439
  oauthSessionId = null;
2392
2440
  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 = "";
2441
+ oauthStartInFlight = false;
2442
+ oauthOpenedInBrowser = false;
2443
+ oauthFlowStartedAtMs = 0;
2444
+ if (oauthRetryBtn) oauthRetryBtn.classList.add("hidden");
2445
+ setOauthOpenButton("");
2446
+ setOauthStep(oauthStepPrepare, "pending");
2447
+ setOauthStep(oauthStepOpen, "pending");
2448
+ setOauthStep(oauthStepComplete, "pending");
2449
+ setOauthStep(oauthStepVerify, "pending");
2450
+ setOauthStatus("Choose OAuth and wait a moment. We will prepare your sign-in automatically.");
2402
2451
  }
2403
2452
 
2404
2453
  (function initLlmAuthDefaults() {
@@ -2407,10 +2456,6 @@ function wizardHtml(defaults) {
2407
2456
  llmAuthModeOauth.checked = true;
2408
2457
  llmAuthModeApiKey.checked = false;
2409
2458
  }
2410
- llmOAuthPasteEl.value = ${JSON.stringify(defaults.llmOAuthPaste || "")};
2411
- if (${defaults.llmOAuthSkipLogin === true ? "true" : "false"}) {
2412
- llmOAuthSkipLoginEl.checked = true;
2413
- }
2414
2459
  applyLlmAuthModeUi();
2415
2460
  })();
2416
2461
 
@@ -2431,6 +2476,9 @@ function wizardHtml(defaults) {
2431
2476
  llmOauthProviderNote.classList.remove("hidden");
2432
2477
  llmApiKeyBlock.classList.add("hidden");
2433
2478
  llmOauthBlock.classList.remove("hidden");
2479
+ if (!oauthWizardLoginDone && !oauthStartInFlight && !oauthSessionId) {
2480
+ void startOauthGuidedFlow();
2481
+ }
2434
2482
  } else {
2435
2483
  void cancelOauthSession();
2436
2484
  resetOauthWizardState();
@@ -2452,13 +2500,158 @@ function wizardHtml(defaults) {
2452
2500
  function hasRequiredInputs() {
2453
2501
  if (!llmCatalogReady || !Boolean(telegramTokenEl.value.trim())) return false;
2454
2502
  if (isOauthMode()) {
2455
- if (oauthWizardLoginDone) return true;
2456
- if (llmOAuthSkipLoginEl.checked) return true;
2457
- return Boolean(llmOAuthPasteEl.value.trim());
2503
+ return oauthWizardLoginDone === true;
2458
2504
  }
2459
2505
  return Boolean(llmProviderEl.value.trim()) && Boolean(llmCredentialEl.value.trim());
2460
2506
  }
2461
2507
 
2508
+ async function pollOauthStatusOnce() {
2509
+ if (!oauthSessionId || !isOauthMode()) return;
2510
+ try {
2511
+ const sessionId = oauthSessionId;
2512
+ const res = await fetch("/api/llm/oauth/status?sessionId=" + encodeURIComponent(sessionId));
2513
+ const data = await res.json().catch(() => ({}));
2514
+ if (!res.ok) {
2515
+ stopOauthPolling();
2516
+ oauthStartInFlight = false;
2517
+ if (data && data.error === "invalid_or_expired_session") {
2518
+ setOauthStep(oauthStepPrepare, "done");
2519
+ setOauthStep(oauthStepOpen, "error");
2520
+ setOauthStep(oauthStepComplete, "error");
2521
+ setOauthStep(oauthStepVerify, "error");
2522
+ setOauthStatus(
2523
+ "Automatic OAuth session expired. This flow requires the wizard and OpenClaw on the same machine and the same browser. Click Try sign-in again.",
2524
+ true,
2525
+ );
2526
+ } else {
2527
+ setOauthStatus((data && (data.message || data.error)) || "Could not read OAuth status.", true);
2528
+ }
2529
+ if (oauthRetryBtn) oauthRetryBtn.classList.remove("hidden");
2530
+ updateStartButtonState();
2531
+ return;
2532
+ }
2533
+ const state = typeof data.state === "string" ? data.state : "unknown";
2534
+ if (state === "awaiting_browser_callback") {
2535
+ setOauthStep(oauthStepPrepare, "done");
2536
+ setOauthStep(oauthStepOpen, oauthOpenedInBrowser ? "done" : "active");
2537
+ setOauthStep(oauthStepComplete, oauthOpenedInBrowser ? "active" : "pending");
2538
+ setOauthStep(oauthStepVerify, oauthOpenedInBrowser ? "active" : "pending");
2539
+ if (oauthOpenedInBrowser) {
2540
+ const elapsed = oauthFlowStartedAtMs > 0 ? Math.floor((Date.now() - oauthFlowStartedAtMs) / 1000) : 0;
2541
+ if (elapsed >= 90) {
2542
+ setOauthStatus(
2543
+ "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.",
2544
+ true,
2545
+ );
2546
+ if (oauthRetryBtn) oauthRetryBtn.classList.remove("hidden");
2547
+ } else {
2548
+ setOauthStatus("Waiting for ChatGPT approval to finish. Return here after you continue.", false);
2549
+ }
2550
+ } else {
2551
+ setOauthStatus("Step 2: click Open ChatGPT sign-in, then complete approval and return here.", false);
2552
+ }
2553
+ updateStartButtonState();
2554
+ return;
2555
+ }
2556
+ stopOauthPolling();
2557
+ oauthStartInFlight = false;
2558
+ if (state === "succeeded") {
2559
+ oauthWizardLoginDone = true;
2560
+ oauthSessionId = null;
2561
+ hideFallbackPaste();
2562
+ setOauthStep(oauthStepPrepare, "done");
2563
+ setOauthStep(oauthStepOpen, "done");
2564
+ setOauthStep(oauthStepComplete, "done");
2565
+ setOauthStep(oauthStepVerify, "done");
2566
+ setOauthStatus(data.message || "ChatGPT is connected. You can start installation now.", false, true);
2567
+ setOauthOpenButton("");
2568
+ if (oauthRetryBtn) oauthRetryBtn.classList.add("hidden");
2569
+ updateStartButtonState();
2570
+ return;
2571
+ }
2572
+ const errorText = data.message || data.error || "OAuth login failed.";
2573
+ setOauthStep(oauthStepPrepare, "done");
2574
+ setOauthStep(oauthStepOpen, "error");
2575
+ setOauthStep(oauthStepComplete, "error");
2576
+ setOauthStep(oauthStepVerify, "error");
2577
+ setOauthStatus(
2578
+ errorText + " Automatic mode only works on the same machine and browser. Click Try sign-in again.",
2579
+ true,
2580
+ );
2581
+ setOauthOpenButton("");
2582
+ if (oauthRetryBtn) oauthRetryBtn.classList.remove("hidden");
2583
+ updateStartButtonState();
2584
+ } catch (err) {
2585
+ stopOauthPolling();
2586
+ oauthStartInFlight = false;
2587
+ setOauthStep(oauthStepPrepare, "done");
2588
+ setOauthStep(oauthStepOpen, "error");
2589
+ setOauthStep(oauthStepComplete, "error");
2590
+ setOauthStep(oauthStepVerify, "error");
2591
+ setOauthStatus(err && err.message ? String(err.message) : "OAuth status request failed.", true);
2592
+ if (oauthRetryBtn) oauthRetryBtn.classList.remove("hidden");
2593
+ updateStartButtonState();
2594
+ }
2595
+ }
2596
+
2597
+ function beginOauthStatusPolling() {
2598
+ stopOauthPolling();
2599
+ oauthPollTimer = setInterval(() => {
2600
+ void pollOauthStatusOnce();
2601
+ }, 1500);
2602
+ void pollOauthStatusOnce();
2603
+ }
2604
+
2605
+ async function startOauthGuidedFlow() {
2606
+ if (!isOauthMode() || oauthStartInFlight) return;
2607
+ oauthStartInFlight = true;
2608
+ oauthWizardLoginDone = false;
2609
+ oauthOpenedInBrowser = false;
2610
+ oauthFlowStartedAtMs = Date.now();
2611
+ if (oauthRetryBtn) oauthRetryBtn.classList.add("hidden");
2612
+ setOauthStep(oauthStepPrepare, "active");
2613
+ setOauthStep(oauthStepOpen, "pending");
2614
+ setOauthStep(oauthStepComplete, "pending");
2615
+ setOauthStep(oauthStepVerify, "pending");
2616
+ setOauthStatus("Preparing ChatGPT sign-in link...");
2617
+ setOauthOpenButton("");
2618
+ updateStartButtonState();
2619
+ try {
2620
+ const res = await fetch("/api/llm/oauth/start", { method: "POST" });
2621
+ const data = await res.json().catch(() => ({}));
2622
+ if (!res.ok) {
2623
+ oauthStartInFlight = false;
2624
+ setOauthStep(oauthStepPrepare, "error");
2625
+ setOauthStep(oauthStepOpen, "error");
2626
+ setOauthStep(oauthStepComplete, "error");
2627
+ setOauthStep(oauthStepVerify, "error");
2628
+ setOauthStatus((data && (data.message || data.error)) || "Could not start OAuth sign-in.", true);
2629
+ if (oauthRetryBtn) oauthRetryBtn.classList.remove("hidden");
2630
+ updateStartButtonState();
2631
+ return;
2632
+ }
2633
+ oauthSessionId = typeof data.sessionId === "string" ? data.sessionId : "";
2634
+ const authUrl = typeof data.authUrl === "string" ? data.authUrl : "";
2635
+ if (!oauthSessionId || !authUrl) {
2636
+ oauthStartInFlight = false;
2637
+ setOauthStep(oauthStepPrepare, "error");
2638
+ setOauthStatus("OpenClaw did not return a valid sign-in URL. Try again.", true);
2639
+ if (oauthRetryBtn) oauthRetryBtn.classList.remove("hidden");
2640
+ return;
2641
+ }
2642
+ setOauthOpenButton(authUrl);
2643
+ setOauthStep(oauthStepPrepare, "done");
2644
+ setOauthStep(oauthStepOpen, "active");
2645
+ setOauthStatus("Step 2: click Open ChatGPT sign-in and continue in this same browser.");
2646
+ beginOauthStatusPolling();
2647
+ } catch (err) {
2648
+ oauthStartInFlight = false;
2649
+ setOauthStep(oauthStepPrepare, "error");
2650
+ setOauthStatus(err && err.message ? String(err.message) : "Could not start OAuth sign-in.", true);
2651
+ if (oauthRetryBtn) oauthRetryBtn.classList.remove("hidden");
2652
+ }
2653
+ }
2654
+
2462
2655
  /** All-or-nothing: 0 or 4 non-empty X fields; partial is invalid. */
2463
2656
  function xWizardFieldsStatus() {
2464
2657
  const fields = [
@@ -2635,8 +2828,8 @@ function wizardHtml(defaults) {
2635
2828
  llmProvider: oauth ? "openai-codex" : llmProviderEl.value.trim(),
2636
2829
  llmModel: llmModelEl.value.trim(),
2637
2830
  llmCredential: oauth ? "" : llmCredentialEl.value.trim(),
2638
- llmOAuthPaste: llmOAuthPasteEl.value.trim(),
2639
- llmOAuthSkipLogin: llmOAuthSkipLoginEl.checked,
2831
+ llmOAuthPaste: "",
2832
+ llmOAuthSkipLogin: oauth ? true : false,
2640
2833
  apiKey: document.getElementById("apiKey").value.trim(),
2641
2834
  telegramToken: document.getElementById("telegramToken").value.trim(),
2642
2835
  referralCode: document.getElementById("referralCode").value.trim(),
@@ -2645,16 +2838,12 @@ function wizardHtml(defaults) {
2645
2838
  xAccessTokenMain: xAccessTokenMainEl.value.trim(),
2646
2839
  xAccessTokenMainSecret: xAccessTokenMainSecretEl.value.trim(),
2647
2840
  };
2648
- if (oauth && oauthWizardLoginDone) {
2649
- payload.llmOAuthSkipLogin = true;
2650
- payload.llmOAuthPaste = "";
2651
- }
2652
2841
  if (oauth) {
2653
- if (!payload.llmOAuthSkipLogin && !payload.llmOAuthPaste && !oauthWizardLoginDone) {
2842
+ if (!oauthWizardLoginDone) {
2654
2843
  stateEl.textContent = "blocked";
2655
2844
  readyEl.textContent = "";
2656
2845
  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.";
2846
+ "Codex OAuth is not completed yet. Use the guided sign-in above in this same browser and wait for the green success message.";
2658
2847
  return;
2659
2848
  }
2660
2849
  } else if (!payload.llmProvider || !payload.llmCredential) {
@@ -2905,150 +3094,65 @@ function wizardHtml(defaults) {
2905
3094
  llmAuthModeApiKey.addEventListener("change", onLlmAuthModeChange);
2906
3095
  llmAuthModeOauth.addEventListener("change", onLlmAuthModeChange);
2907
3096
  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;
3097
+ if (oauthOpenUrlBtn) {
3098
+ oauthOpenUrlBtn.addEventListener("click", () => {
3099
+ if (!oauthSessionId || oauthWizardLoginDone) return;
3100
+ oauthOpenedInBrowser = true;
3101
+ setOauthStep(oauthStepOpen, "done");
3102
+ setOauthStep(oauthStepComplete, "active");
3103
+ setOauthStep(oauthStepVerify, "active");
3104
+ setOauthStatus("Complete ChatGPT approval in this browser, then return here. We detect completion automatically.");
3105
+ showFallbackPasteAfterDelay(15_000);
2958
3106
  });
2959
3107
  }
2960
3108
 
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.";
3109
+ if (oauthFallbackSubmitBtn) {
3110
+ oauthFallbackSubmitBtn.addEventListener("click", async () => {
3111
+ const raw = (oauthFallbackUrlInput && oauthFallbackUrlInput.value || "").trim();
3112
+ if (!raw || !raw.includes("code=")) {
3113
+ if (oauthFallbackError) {
3114
+ oauthFallbackError.textContent = "Paste the full URL from the browser address bar. It must contain code=…";
3115
+ oauthFallbackError.classList.remove("hidden");
2968
3116
  }
2969
3117
  return;
2970
3118
  }
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
- }
3119
+ if (oauthFallbackError) oauthFallbackError.classList.add("hidden");
3120
+ oauthFallbackSubmitBtn.disabled = true;
3121
+ oauthFallbackSubmitBtn.textContent = "Submitting…";
2983
3122
  try {
2984
- const res = await fetch("/api/llm/oauth/submit", {
3123
+ const res = await fetch("/api/llm/oauth/submit-callback-url", {
2985
3124
  method: "POST",
2986
3125
  headers: { "content-type": "application/json" },
2987
- body: JSON.stringify({ sessionId: oauthSessionId, paste }),
3126
+ body: JSON.stringify({ sessionId: oauthSessionId, callbackUrl: raw }),
2988
3127
  });
2989
3128
  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.";
3129
+ if (res.ok && data.ok) {
3130
+ hideFallbackPaste();
3131
+ setOauthStatus("Callback received! Waiting for OpenClaw to finish…", false);
3132
+ } else {
3133
+ if (oauthFallbackError) {
3134
+ oauthFallbackError.textContent = data.message || data.error || "Could not submit the URL. Try again.";
3135
+ oauthFallbackError.classList.remove("hidden");
2994
3136
  }
2995
- oauthSubmitBtn.disabled = false;
2996
- return;
2997
3137
  }
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
3138
  } catch (err) {
3008
- if (oauthFlowStatus) {
3009
- oauthFlowStatus.className = "err-banner";
3010
- oauthFlowStatus.textContent = err && err.message ? String(err.message) : "Request failed.";
3139
+ if (oauthFallbackError) {
3140
+ oauthFallbackError.textContent = err.message || "Request failed.";
3141
+ oauthFallbackError.classList.remove("hidden");
3011
3142
  }
3012
- oauthSubmitBtn.disabled = false;
3143
+ } finally {
3144
+ oauthFallbackSubmitBtn.disabled = false;
3145
+ oauthFallbackSubmitBtn.textContent = "Submit URL";
3013
3146
  }
3014
3147
  });
3015
3148
  }
3016
3149
 
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
- }
3033
- });
3034
- }
3035
-
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);
3150
+ if (oauthRetryBtn) {
3151
+ oauthRetryBtn.addEventListener("click", async () => {
3152
+ await cancelOauthSession();
3153
+ resetOauthWizardState();
3154
+ if (isOauthMode()) {
3155
+ await startOauthGuidedFlow();
3052
3156
  }
3053
3157
  });
3054
3158
  }
@@ -3095,16 +3199,69 @@ async function cmdInstall(args) {
3095
3199
  let running = false;
3096
3200
  let shuttingDown = false;
3097
3201
 
3098
- /** In-browser Codex OAuth: one pending `openclaw models auth login` child waiting for stdin (same flow as CLI). */
3202
+ /** Guided Codex OAuth sessions keyed by sessionId. */
3099
3203
  const oauthSessions = new Map();
3100
3204
  const OPENAI_OAUTH_AUTHORIZE_RE = /https:\/\/auth\.openai\.com\/oauth\/authorize\S*/;
3101
3205
  const oauthSessionTtlMs = 15 * 60 * 1000;
3102
3206
 
3207
+ // Long-lived callback proxy on port 1455. Bound at wizard startup so
3208
+ // Cursor/VSCode auto-forwards it to the user's laptop before they begin
3209
+ // the OAuth flow. When ChatGPT redirects the browser to localhost:1455,
3210
+ // this proxy catches the callback and feeds the code to the active
3211
+ // openclaw child process.
3212
+ let oauthCallbackProxy = null;
3213
+
3214
+ function findActiveOauthSession() {
3215
+ for (const [, s] of oauthSessions) {
3216
+ if (s.status === "awaiting_browser_callback" && s.child) return s;
3217
+ }
3218
+ return null;
3219
+ }
3220
+
3221
+ function startCallbackProxy() {
3222
+ if (oauthCallbackProxy) return;
3223
+ try {
3224
+ oauthCallbackProxy = createServer((cbReq, cbRes) => {
3225
+ const s = findActiveOauthSession();
3226
+ const fullUrl = `http://localhost:1455${cbReq.url}`;
3227
+ if (s && cbReq.url && cbReq.url.includes("code=")) {
3228
+ s.updatedAt = Date.now();
3229
+ try {
3230
+ if (s.child && s.child.stdin && !s.child.stdin.writableEnded && !s.child.stdin.destroyed) {
3231
+ s.child.stdin.write(`${fullUrl}\n`, () => {
3232
+ setTimeout(() => {
3233
+ try {
3234
+ if (s.child && s.child.stdin && !s.child.stdin.destroyed && !s.child.stdin.writableEnded) {
3235
+ s.child.stdin.end();
3236
+ }
3237
+ } catch { /* ignore */ }
3238
+ }, 100);
3239
+ });
3240
+ }
3241
+ } catch { /* ignore */ }
3242
+ cbRes.statusCode = 200;
3243
+ cbRes.setHeader("content-type", "text/html; charset=utf-8");
3244
+ 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>`);
3245
+ } else {
3246
+ cbRes.statusCode = 200;
3247
+ cbRes.setHeader("content-type", "text/html; charset=utf-8");
3248
+ 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>`);
3249
+ }
3250
+ });
3251
+ oauthCallbackProxy.listen(1455, "127.0.0.1");
3252
+ oauthCallbackProxy.on("error", () => {
3253
+ oauthCallbackProxy = null;
3254
+ });
3255
+ } catch {
3256
+ oauthCallbackProxy = null;
3257
+ }
3258
+ }
3259
+
3103
3260
  function killOauthSession(sessionId, signal = "SIGTERM") {
3104
3261
  const s = oauthSessions.get(sessionId);
3105
3262
  if (!s) return;
3106
3263
  try {
3107
- s.child.kill(signal);
3264
+ if (s.child) s.child.kill(signal);
3108
3265
  } catch {
3109
3266
  /* ignore */
3110
3267
  }
@@ -3114,7 +3271,8 @@ async function cmdInstall(args) {
3114
3271
  function pruneExpiredOauthSessions() {
3115
3272
  const now = Date.now();
3116
3273
  for (const [id, s] of oauthSessions) {
3117
- if (now - s.createdAt > oauthSessionTtlMs) {
3274
+ const anchor = s.updatedAt || s.createdAt || now;
3275
+ if (now - anchor > oauthSessionTtlMs) {
3118
3276
  killOauthSession(id);
3119
3277
  }
3120
3278
  }
@@ -3183,6 +3341,35 @@ async function cmdInstall(args) {
3183
3341
  return;
3184
3342
  }
3185
3343
 
3344
+ if (req.method === "GET" && req.url.startsWith("/api/llm/oauth/status")) {
3345
+ pruneExpiredOauthSessions();
3346
+ let sessionId = "";
3347
+ try {
3348
+ const u = new URL(req.url, "http://127.0.0.1");
3349
+ sessionId = String(u.searchParams.get("sessionId") || "").trim();
3350
+ } catch {
3351
+ sessionId = "";
3352
+ }
3353
+ if (!sessionId) {
3354
+ respondJson(400, { ok: false, error: "session_id_required" });
3355
+ return;
3356
+ }
3357
+ const s = oauthSessions.get(sessionId);
3358
+ if (!s) {
3359
+ respondJson(404, { ok: false, error: "invalid_or_expired_session", message: "OAuth session expired. Start sign-in again." });
3360
+ return;
3361
+ }
3362
+ respondJson(200, {
3363
+ ok: true,
3364
+ state: s.status || "unknown",
3365
+ message: s.message || "",
3366
+ authUrl: s.authUrl || "",
3367
+ exitCode: typeof s.exitCode === "number" ? s.exitCode : null,
3368
+ detail: s.detail || "",
3369
+ });
3370
+ return;
3371
+ }
3372
+
3186
3373
  if (req.method === "POST" && req.url === "/api/llm/oauth/start") {
3187
3374
  pruneExpiredOauthSessions();
3188
3375
  if (running) {
@@ -3219,7 +3406,7 @@ async function cmdInstall(args) {
3219
3406
  ok: false,
3220
3407
  error: "oauth_url_timeout",
3221
3408
  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.",
3409
+ "OpenClaw did not provide a ChatGPT sign-in URL in time. Try again.",
3223
3410
  });
3224
3411
  }, 45_000);
3225
3412
 
@@ -3229,7 +3416,17 @@ async function cmdInstall(args) {
3229
3416
  if (!m || !m[0]) return;
3230
3417
  clearTimeout(urlTimeout);
3231
3418
  responded = true;
3232
- oauthSessions.set(sessionId, { child, createdAt: Date.now(), submitted: false });
3419
+ oauthSessions.set(sessionId, {
3420
+ child,
3421
+ createdAt: Date.now(),
3422
+ updatedAt: Date.now(),
3423
+ status: "awaiting_browser_callback",
3424
+ authUrl: m[0],
3425
+ message: "Sign in in this same browser. OpenClaw is waiting for callback...",
3426
+ detail: "",
3427
+ exitCode: null,
3428
+ submitted: false,
3429
+ });
3233
3430
  respondJson(200, { ok: true, sessionId, authUrl: m[0] });
3234
3431
  };
3235
3432
 
@@ -3258,14 +3455,25 @@ async function cmdInstall(args) {
3258
3455
  error: "oauth_login_exited_early",
3259
3456
  exitCode: code,
3260
3457
  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.",
3458
+ "OpenClaw exited before showing a sign-in URL. Start OAuth again and keep the flow on this same machine/browser.",
3262
3459
  detail: stripAnsi(combined).slice(-4000),
3263
3460
  });
3264
3461
  return;
3265
3462
  }
3266
3463
  const pending = oauthSessions.get(sessionId);
3267
- if (pending && !pending.submitted) {
3268
- oauthSessions.delete(sessionId);
3464
+ if (pending) {
3465
+ pending.child = null;
3466
+ pending.updatedAt = Date.now();
3467
+ pending.exitCode = typeof code === "number" ? code : null;
3468
+ pending.detail = stripAnsi(combined).slice(-4000);
3469
+ if (code === 0) {
3470
+ pending.status = "succeeded";
3471
+ pending.message = "ChatGPT OAuth completed successfully.";
3472
+ } else {
3473
+ pending.status = "failed";
3474
+ pending.message =
3475
+ "OpenClaw OAuth did not complete. Try again — make sure you continue in the same browser that opened the sign-in link.";
3476
+ }
3269
3477
  }
3270
3478
  });
3271
3479
  return;
@@ -3353,6 +3561,41 @@ async function cmdInstall(args) {
3353
3561
  return;
3354
3562
  }
3355
3563
 
3564
+ if (req.method === "POST" && req.url === "/api/llm/oauth/submit-callback-url") {
3565
+ const body = await parseJsonBody(req).catch(() => ({}));
3566
+ const sessionId = typeof body.sessionId === "string" ? body.sessionId : "";
3567
+ const callbackUrl = typeof body.callbackUrl === "string" ? body.callbackUrl.trim() : "";
3568
+ if (!callbackUrl || !callbackUrl.includes("code=")) {
3569
+ respondJson(400, { ok: false, error: "invalid_url", message: "URL must contain a code= parameter." });
3570
+ return;
3571
+ }
3572
+ const s = sessionId ? oauthSessions.get(sessionId) : findActiveOauthSession();
3573
+ if (!s || !s.child) {
3574
+ respondJson(404, { ok: false, error: "no_active_session", message: "No active OAuth session. Click Try sign-in again." });
3575
+ return;
3576
+ }
3577
+ try {
3578
+ if (s.child.stdin && !s.child.stdin.writableEnded && !s.child.stdin.destroyed) {
3579
+ s.child.stdin.write(`${callbackUrl}\n`, () => {
3580
+ setTimeout(() => {
3581
+ try {
3582
+ if (s.child && s.child.stdin && !s.child.stdin.destroyed && !s.child.stdin.writableEnded) {
3583
+ s.child.stdin.end();
3584
+ }
3585
+ } catch { /* ignore */ }
3586
+ }, 100);
3587
+ });
3588
+ s.updatedAt = Date.now();
3589
+ respondJson(200, { ok: true, message: "Callback URL submitted. Waiting for OpenClaw to complete…" });
3590
+ } else {
3591
+ respondJson(500, { ok: false, error: "stdin_closed", message: "OpenClaw process stdin is closed. Click Try sign-in again." });
3592
+ }
3593
+ } catch (err) {
3594
+ respondJson(500, { ok: false, error: "write_error", message: err.message || "Failed to write to OpenClaw." });
3595
+ }
3596
+ return;
3597
+ }
3598
+
3356
3599
  if (req.method === "POST" && req.url === "/api/llm/oauth/cancel") {
3357
3600
  const body = await parseJsonBody(req).catch(() => ({}));
3358
3601
  const sessionId = typeof body.sessionId === "string" ? body.sessionId : "";
@@ -3522,12 +3765,21 @@ async function cmdInstall(args) {
3522
3765
  server.listen(defaults.port, "127.0.0.1", resolve);
3523
3766
  });
3524
3767
 
3768
+ // Start the OAuth callback proxy on port 1455 early so Cursor/VSCode
3769
+ // auto-forwards it to the user's laptop before the OAuth flow begins.
3770
+ startCallbackProxy();
3771
+ if (oauthCallbackProxy) {
3772
+ printInfo(`OAuth callback proxy listening on http://127.0.0.1:1455`);
3773
+ }
3774
+
3525
3775
  const url = `http://127.0.0.1:${defaults.port}`;
3526
3776
  printSuccess(`Installer wizard is running at ${url}`);
3527
3777
  if (!openBrowser(url)) {
3528
3778
  printInfo(`Open this URL in your browser: ${url}`);
3529
3779
  }
3530
3780
  printInfo("Press Ctrl+C to stop the wizard server.");
3781
+ printInfo(`If you are on a remote VPS, forward both ports from your local machine:`);
3782
+ printInfo(` ssh -L ${defaults.port}:127.0.0.1:${defaults.port} -L 1455:127.0.0.1:1455 <user>@<your-vps>`);
3531
3783
  }
3532
3784
 
3533
3785
  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.78",
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.78"
21
21
  },
22
22
  "keywords": [
23
23
  "traderclaw",