traderclaw-cli 1.0.74 → 1.0.76

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 = "";
@@ -769,7 +769,7 @@ async function cmdSetup(args) {
769
769
  "\n Optional: enter a referral code for bonus access time (24h extra when valid). Press Enter to skip.\n",
770
770
  );
771
771
  printInfo(
772
- " Benefits: extra trial time now; referring others later earns +8h per user who completes at least one trade with the agent.\n",
772
+ " Benefits: extra trial time now; referring others later earns +24h per user who completes at least one trade with the agent.\n",
773
773
  );
774
774
  referralCodeArg = await prompt("Referral code (optional)", "");
775
775
  }
@@ -2075,6 +2075,21 @@ function wizardHtml(defaults) {
2075
2075
  .muted a:hover { color:#c5e5ff; }
2076
2076
  .info-dot { display:inline-flex; align-items:center; justify-content:center; width:18px; height:18px; border-radius:50%; background:#22315a; color:#9cb0de; font-size:11px; font-weight:700; cursor:help; flex-shrink:0; }
2077
2077
  @keyframes spin { to { transform:rotate(360deg); } }
2078
+ .oauth-flow { background:#0d1530; border:1px solid #334a87; border-radius:10px; padding:14px; margin-top:10px; }
2079
+ .oauth-flow ol { margin:8px 0 0 18px; padding:0; color:#c5d7f5; font-size:13px; line-height:1.5; }
2080
+ .oauth-flow li { margin-bottom:8px; }
2081
+ .oauth-row { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-top:10px; }
2082
+ .oauth-row input[readonly] { flex:1 1 280px; font-size:12px; }
2083
+ .oauth-actions { display:flex; gap:8px; flex-wrap:wrap; margin-top:10px; }
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; }
2091
+ .ok-banner { color:#78f0a9; font-size:13px; margin-top:8px; }
2092
+ .err-banner { color:#ff6b6b; font-size:13px; margin-top:8px; }
2078
2093
  </style>
2079
2094
  </head>
2080
2095
  <body>
@@ -2120,13 +2135,53 @@ function wizardHtml(defaults) {
2120
2135
  <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>
2121
2136
  </div>
2122
2137
  <div style="margin-top:12px;" id="llmOauthBlock" class="hidden">
2123
- <label>Paste authorization code or full redirect URL</label>
2124
- <textarea id="llmOAuthPaste" autocomplete="off" placeholder="After the installer prints an OAuth URL in the log, sign in locally and paste the code or full callback URL here. Leave empty if you use the option below."></textarea>
2125
- <label style="display:flex; align-items:flex-start; gap:8px; font-size:13px; color:#9cb0de; margin-top:8px; cursor:pointer;">
2138
+ <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
+ </p>
2141
+ <div class="oauth-terminal-box">
2142
+ <strong style="color:#8ef5d0;">Recommended: sign in from a terminal on this machine</strong>
2143
+ <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>
2147
+ </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>
2151
+ </div>
2152
+ </div>
2153
+ <label style="display:flex; align-items:flex-start; gap:10px; font-size:14px; color:#e8eef9; margin-top:16px; cursor:pointer;">
2126
2154
  <input id="llmOAuthSkipLogin" type="checkbox" style="width:auto; margin-top:3px;" />
2127
- <span>I already ran <code>openclaw models auth login --provider openai-codex</code> on this machine</span>
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>
2128
2156
  </label>
2129
- <p class="muted">If login hangs over SSH, complete OAuth in a normal shell first, then enable the checkbox above and start again. Live install logs will show the authorize URL when the CLI prints it.</p>
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>
2130
2185
  </div>
2131
2186
  <p class="muted" id="llmLoadState" aria-live="polite">Loading LLM provider catalog...</p>
2132
2187
  <div id="llmLoadingHint" class="loading-hint" role="status" aria-live="polite">
@@ -2174,10 +2229,10 @@ function wizardHtml(defaults) {
2174
2229
  </div>
2175
2230
  <div style="margin-top:12px;">
2176
2231
  <label style="display:flex;align-items:center;gap:8px;">Referral code (optional)
2177
- <span class="info-dot" title="Included access: 24 hours for every new account. Add a valid referral code for an extra 24 hours. Refer others: when they complete at least one trade with the agent, you earn +8 hours per active referral. When your access window ends, you will need to stake or keep referring to continue.">i</span>
2232
+ <span class="info-dot" title="Included access: 24 hours for every new account. Add a valid referral code for an extra 24 hours. Refer others: when they complete at least one trade with the agent, you earn +24 hours per active referral. When your access window ends, you will need to stake or keep referring to continue.">i</span>
2178
2233
  </label>
2179
2234
  <input id="referralCode" type="text" maxlength="16" autocomplete="off" placeholder="e.g. ABCD1234" />
2180
- <p class="muted">If you have a friend’s code, enter it here. The setup command below will include it for <code>traderclaw setup</code>. If the server rejects the code, clear this field or fix it and copy the updated command or run <code>traderclaw setup</code> again and enter a valid code or leave referral blank when prompted.</p>
2235
+ <p class="muted">Included access: 24 hours for every new account. Add a valid referral code for an extra 24 hours. Refer others: when they complete at least one trade with the agent, you earn +24 hours per active referral. When your access window ends, you will need to stake or keep referring to continue.</p>
2181
2236
  </div>
2182
2237
  <button id="start" disabled>Start Installation</button>
2183
2238
  </div>
@@ -2300,6 +2355,51 @@ function wizardHtml(defaults) {
2300
2355
  let pollIntervalMs = 1200;
2301
2356
  let installLocked = false;
2302
2357
  let savedApiKeyProvider = "";
2358
+ let oauthSessionId = null;
2359
+ let oauthWizardLoginDone = false;
2360
+
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
+ const oauthOpenUrlBtn = document.getElementById("oauthOpenUrlBtn");
2367
+ const oauthFlowStatus = document.getElementById("oauthFlowStatus");
2368
+
2369
+ async function cancelOauthSession() {
2370
+ if (!oauthSessionId) {
2371
+ try {
2372
+ await fetch("/api/llm/oauth/cancel", { method: "POST", headers: { "content-type": "application/json" }, body: "{}" });
2373
+ } catch {
2374
+ /* ignore */
2375
+ }
2376
+ return;
2377
+ }
2378
+ try {
2379
+ await fetch("/api/llm/oauth/cancel", {
2380
+ method: "POST",
2381
+ headers: { "content-type": "application/json" },
2382
+ body: JSON.stringify({ sessionId: oauthSessionId }),
2383
+ });
2384
+ } catch {
2385
+ /* ignore */
2386
+ }
2387
+ oauthSessionId = null;
2388
+ }
2389
+
2390
+ function resetOauthWizardState() {
2391
+ oauthSessionId = null;
2392
+ 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 = "";
2402
+ }
2303
2403
 
2304
2404
  (function initLlmAuthDefaults() {
2305
2405
  const mode = ${JSON.stringify(defaults.llmAuthMode || "api_key")};
@@ -2332,6 +2432,8 @@ function wizardHtml(defaults) {
2332
2432
  llmApiKeyBlock.classList.add("hidden");
2333
2433
  llmOauthBlock.classList.remove("hidden");
2334
2434
  } else {
2435
+ void cancelOauthSession();
2436
+ resetOauthWizardState();
2335
2437
  llmProviderWrap.classList.remove("hidden");
2336
2438
  llmOauthProviderNote.classList.add("hidden");
2337
2439
  llmApiKeyBlock.classList.remove("hidden");
@@ -2350,6 +2452,7 @@ function wizardHtml(defaults) {
2350
2452
  function hasRequiredInputs() {
2351
2453
  if (!llmCatalogReady || !Boolean(telegramTokenEl.value.trim())) return false;
2352
2454
  if (isOauthMode()) {
2455
+ if (oauthWizardLoginDone) return true;
2353
2456
  if (llmOAuthSkipLoginEl.checked) return true;
2354
2457
  return Boolean(llmOAuthPasteEl.value.trim());
2355
2458
  }
@@ -2542,12 +2645,16 @@ function wizardHtml(defaults) {
2542
2645
  xAccessTokenMain: xAccessTokenMainEl.value.trim(),
2543
2646
  xAccessTokenMainSecret: xAccessTokenMainSecretEl.value.trim(),
2544
2647
  };
2648
+ if (oauth && oauthWizardLoginDone) {
2649
+ payload.llmOAuthSkipLogin = true;
2650
+ payload.llmOAuthPaste = "";
2651
+ }
2545
2652
  if (oauth) {
2546
- if (!payload.llmOAuthSkipLogin && !payload.llmOAuthPaste) {
2653
+ if (!payload.llmOAuthSkipLogin && !payload.llmOAuthPaste && !oauthWizardLoginDone) {
2547
2654
  stateEl.textContent = "blocked";
2548
2655
  readyEl.textContent = "";
2549
2656
  manualEl.textContent =
2550
- "Codex OAuth: paste the authorization code or full redirect URL, or check the box if you already ran openclaw models auth login on this machine.";
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.";
2551
2658
  return;
2552
2659
  }
2553
2660
  } else if (!payload.llmProvider || !payload.llmCredential) {
@@ -2800,6 +2907,152 @@ function wizardHtml(defaults) {
2800
2907
  llmCredentialEl.addEventListener("input", updateStartButtonState);
2801
2908
  llmOAuthPasteEl.addEventListener("input", updateStartButtonState);
2802
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
+ }
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);
3052
+ }
3053
+ });
3054
+ }
3055
+
2803
3056
  telegramTokenEl.addEventListener("input", updateStartButtonState);
2804
3057
  xConsumerKeyEl.addEventListener("input", updateStartButtonState);
2805
3058
  xConsumerSecretEl.addEventListener("input", updateStartButtonState);
@@ -2821,7 +3074,8 @@ async function cmdInstall(args) {
2821
3074
  }
2822
3075
 
2823
3076
  const defaults = parseInstallWizardArgs(args);
2824
- const { createInstallerStepEngine, assertWizardXCredentials } = await import("./installer-step-engine.mjs");
3077
+ const { createInstallerStepEngine, assertWizardXCredentials, spawnOpenClawCodexAuthLoginChild } =
3078
+ await import("./installer-step-engine.mjs");
2825
3079
  const modeConfig = {
2826
3080
  pluginPackage: "solana-traderclaw",
2827
3081
  pluginId: "solana-trader",
@@ -2841,6 +3095,31 @@ async function cmdInstall(args) {
2841
3095
  let running = false;
2842
3096
  let shuttingDown = false;
2843
3097
 
3098
+ /** In-browser Codex OAuth: one pending `openclaw models auth login` child waiting for stdin (same flow as CLI). */
3099
+ const oauthSessions = new Map();
3100
+ const OPENAI_OAUTH_AUTHORIZE_RE = /https:\/\/auth\.openai\.com\/oauth\/authorize\S*/;
3101
+ const oauthSessionTtlMs = 15 * 60 * 1000;
3102
+
3103
+ function killOauthSession(sessionId, signal = "SIGTERM") {
3104
+ const s = oauthSessions.get(sessionId);
3105
+ if (!s) return;
3106
+ try {
3107
+ s.child.kill(signal);
3108
+ } catch {
3109
+ /* ignore */
3110
+ }
3111
+ oauthSessions.delete(sessionId);
3112
+ }
3113
+
3114
+ function pruneExpiredOauthSessions() {
3115
+ const now = Date.now();
3116
+ for (const [id, s] of oauthSessions) {
3117
+ if (now - s.createdAt > oauthSessionTtlMs) {
3118
+ killOauthSession(id);
3119
+ }
3120
+ }
3121
+ }
3122
+
2844
3123
  const server = createServer(async (req, res) => {
2845
3124
  const respondJson = (code, payload) => {
2846
3125
  res.statusCode = code;
@@ -2904,6 +3183,190 @@ async function cmdInstall(args) {
2904
3183
  return;
2905
3184
  }
2906
3185
 
3186
+ if (req.method === "POST" && req.url === "/api/llm/oauth/start") {
3187
+ pruneExpiredOauthSessions();
3188
+ if (running) {
3189
+ respondJson(409, { ok: false, error: "install_in_progress" });
3190
+ return;
3191
+ }
3192
+ if (!commandExists("openclaw")) {
3193
+ respondJson(503, {
3194
+ ok: false,
3195
+ error: "openclaw_not_in_path",
3196
+ message: "Install OpenClaw on this machine first (e.g. npm install -g openclaw), then try again.",
3197
+ });
3198
+ return;
3199
+ }
3200
+ for (const id of [...oauthSessions.keys()]) {
3201
+ killOauthSession(id);
3202
+ }
3203
+
3204
+ const sessionId = randomUUID();
3205
+ const child = spawnOpenClawCodexAuthLoginChild();
3206
+
3207
+ let combined = "";
3208
+ let responded = false;
3209
+ const urlTimeout = setTimeout(() => {
3210
+ if (responded) return;
3211
+ responded = true;
3212
+ try {
3213
+ child.kill("SIGTERM");
3214
+ } catch {
3215
+ /* ignore */
3216
+ }
3217
+ oauthSessions.delete(sessionId);
3218
+ respondJson(504, {
3219
+ ok: false,
3220
+ error: "oauth_url_timeout",
3221
+ 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.",
3223
+ });
3224
+ }, 45_000);
3225
+
3226
+ const trySendUrl = () => {
3227
+ if (responded) return;
3228
+ const m = combined.match(OPENAI_OAUTH_AUTHORIZE_RE);
3229
+ if (!m || !m[0]) return;
3230
+ clearTimeout(urlTimeout);
3231
+ responded = true;
3232
+ oauthSessions.set(sessionId, { child, createdAt: Date.now(), submitted: false });
3233
+ respondJson(200, { ok: true, sessionId, authUrl: m[0] });
3234
+ };
3235
+
3236
+ child.stdout?.on("data", (d) => {
3237
+ combined += d.toString();
3238
+ trySendUrl();
3239
+ });
3240
+ child.stderr?.on("data", (d) => {
3241
+ combined += d.toString();
3242
+ trySendUrl();
3243
+ });
3244
+ child.on("error", (err) => {
3245
+ clearTimeout(urlTimeout);
3246
+ if (responded) return;
3247
+ responded = true;
3248
+ oauthSessions.delete(sessionId);
3249
+ respondJson(500, { ok: false, error: "spawn_failed", message: err.message });
3250
+ });
3251
+ child.on("close", (code) => {
3252
+ clearTimeout(urlTimeout);
3253
+ if (!responded) {
3254
+ responded = true;
3255
+ oauthSessions.delete(sessionId);
3256
+ respondJson(500, {
3257
+ ok: false,
3258
+ error: "oauth_login_exited_early",
3259
+ exitCode: code,
3260
+ 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.",
3262
+ detail: stripAnsi(combined).slice(-4000),
3263
+ });
3264
+ return;
3265
+ }
3266
+ const pending = oauthSessions.get(sessionId);
3267
+ if (pending && !pending.submitted) {
3268
+ oauthSessions.delete(sessionId);
3269
+ }
3270
+ });
3271
+ return;
3272
+ }
3273
+
3274
+ if (req.method === "POST" && req.url === "/api/llm/oauth/submit") {
3275
+ const body = await parseJsonBody(req).catch(() => ({}));
3276
+ const sessionId = typeof body.sessionId === "string" ? body.sessionId : "";
3277
+ const paste = typeof body.paste === "string" ? body.paste.trim() : "";
3278
+ if (!sessionId || !oauthSessions.has(sessionId)) {
3279
+ respondJson(400, { ok: false, error: "invalid_or_expired_session", message: "Click “Get ChatGPT sign-in link” again." });
3280
+ return;
3281
+ }
3282
+ if (!paste) {
3283
+ respondJson(400, { ok: false, error: "paste_required" });
3284
+ return;
3285
+ }
3286
+ const s = oauthSessions.get(sessionId);
3287
+ s.submitted = true;
3288
+ const { child } = s;
3289
+
3290
+ await new Promise((resolve) => {
3291
+ let settled = false;
3292
+ let submitTimeout;
3293
+ const finish = (httpCode, payload) => {
3294
+ if (settled) return;
3295
+ settled = true;
3296
+ clearTimeout(submitTimeout);
3297
+ oauthSessions.delete(sessionId);
3298
+ respondJson(httpCode, payload);
3299
+ resolve();
3300
+ };
3301
+
3302
+ submitTimeout = setTimeout(() => {
3303
+ killOauthSession(sessionId);
3304
+ finish(504, {
3305
+ ok: false,
3306
+ error: "oauth_submit_timeout",
3307
+ message: "Login did not finish in time. Try again or complete login in a normal terminal.",
3308
+ });
3309
+ }, 300_000);
3310
+
3311
+ child.once("close", (code) => {
3312
+ if (code === 0) {
3313
+ finish(200, { ok: true });
3314
+ } else {
3315
+ finish(500, {
3316
+ ok: false,
3317
+ error: "oauth_login_failed",
3318
+ exitCode: code,
3319
+ message: "OpenClaw rejected the pasted URL or code. Paste the full redirect URL from the browser address bar, or run login in a terminal.",
3320
+ });
3321
+ }
3322
+ });
3323
+
3324
+ try {
3325
+ const line = `${paste}\n`;
3326
+ if (child.stdin?.writableEnded || child.stdin?.destroyed) {
3327
+ killOauthSession(sessionId);
3328
+ finish(500, { ok: false, error: "stdin_closed", message: "The login session closed before paste could be sent." });
3329
+ return;
3330
+ }
3331
+ child.stdin.write(line, (err) => {
3332
+ if (err) {
3333
+ killOauthSession(sessionId);
3334
+ finish(500, { ok: false, error: "stdin_write_failed", message: err?.message || String(err) });
3335
+ return;
3336
+ }
3337
+ // End stdin so the PTY/readline layer forwards the line and the CLI can proceed (plain `script` often buffers otherwise).
3338
+ setTimeout(() => {
3339
+ try {
3340
+ if (child.stdin && !child.stdin.destroyed && !child.stdin.writableEnded) {
3341
+ child.stdin.end();
3342
+ }
3343
+ } catch {
3344
+ /* ignore */
3345
+ }
3346
+ }, 100);
3347
+ });
3348
+ } catch (err) {
3349
+ killOauthSession(sessionId);
3350
+ finish(500, { ok: false, error: "stdin_write_failed", message: err?.message || String(err) });
3351
+ }
3352
+ });
3353
+ return;
3354
+ }
3355
+
3356
+ if (req.method === "POST" && req.url === "/api/llm/oauth/cancel") {
3357
+ const body = await parseJsonBody(req).catch(() => ({}));
3358
+ const sessionId = typeof body.sessionId === "string" ? body.sessionId : "";
3359
+ if (sessionId && oauthSessions.has(sessionId)) {
3360
+ killOauthSession(sessionId);
3361
+ } else {
3362
+ for (const id of [...oauthSessions.keys()]) {
3363
+ killOauthSession(id);
3364
+ }
3365
+ }
3366
+ respondJson(200, { ok: true });
3367
+ return;
3368
+ }
3369
+
2907
3370
  if (req.method === "POST" && req.url === "/api/finish") {
2908
3371
  if (running || runtime.status !== "completed") {
2909
3372
  respondJson(409, { ok: false, error: "wizard_not_completed" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "traderclaw-cli",
3
- "version": "1.0.74",
3
+ "version": "1.0.76",
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.74"
20
+ "solana-traderclaw": "^1.0.76"
21
21
  },
22
22
  "keywords": [
23
23
  "traderclaw",