traderclaw-cli 1.0.74 → 1.0.75

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.
@@ -6,7 +6,7 @@ import { dirname, join } from "path";
6
6
  import { fileURLToPath, pathToFileURL } from "url";
7
7
  import { homedir } from "os";
8
8
  import { randomUUID, createPrivateKey, sign as cryptoSign } from "crypto";
9
- import { execFile, execSync } from "child_process";
9
+ import { execFile, execSync, spawn } from "child_process";
10
10
  import { promisify } from "util";
11
11
  import { createServer } from "http";
12
12
  import { resolvePluginPackageRoot } from "./resolve-plugin-root.mjs";
@@ -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,15 @@ 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
+ .ok-banner { color:#78f0a9; font-size:13px; margin-top:8px; }
2086
+ .err-banner { color:#ff6b6b; font-size:13px; margin-top:8px; }
2078
2087
  </style>
2079
2088
  </head>
2080
2089
  <body>
@@ -2120,13 +2129,36 @@ function wizardHtml(defaults) {
2120
2129
  <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
2130
  </div>
2122
2131
  <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>
2132
+ <p class="muted" style="margin-bottom:10px;">
2133
+ Follow the flow below to sign in with ChatGPT and submit your login to OpenClaw.
2134
+ </p>
2135
+ <div class="oauth-flow">
2136
+ <strong style="color:#9ee6ff;">Guided sign-in (recommended)</strong>
2137
+ <ol>
2138
+ <li>Click <strong>Get ChatGPT sign-in link</strong> — we run the same command as OpenClaw and show the long <code>https://auth.openai.com/oauth/authorize?…</code> URL here.</li>
2139
+ <li>Open that URL in your <strong>local</strong> browser (on your PC if this is SSH), sign in with ChatGPT.</li>
2140
+ <li>After login, copy either the <strong>full URL</strong> from the address bar or the <strong>code</strong> from the prompt, and paste into the box below.</li>
2141
+ <li>Click <strong>Submit to OpenClaw</strong>. When it succeeds, you can start installation — no need to guess what to paste.</li>
2142
+ </ol>
2143
+ <div class="oauth-actions">
2144
+ <button type="button" id="oauthGetLinkBtn" class="secondary">1. Get ChatGPT sign-in link</button>
2145
+ <button type="button" id="oauthSubmitBtn" class="secondary" disabled>3. Submit to OpenClaw</button>
2146
+ </div>
2147
+ <div id="oauthUrlRow" class="oauth-row hidden">
2148
+ <label style="flex:1 1 100%; font-size:12px; color:#9cb0de;">Sign-in URL (open this in your browser — this is not what you paste back)</label>
2149
+ <input type="text" id="oauthUrlDisplay" readonly placeholder="Click “Get ChatGPT sign-in link” first" />
2150
+ <button type="button" id="oauthCopyUrlBtn" class="secondary" disabled>Copy URL</button>
2151
+ <a id="oauthOpenUrlBtn" class="secondary" href="#" target="_blank" rel="noopener noreferrer" style="display:inline-block;padding:10px 14px;border-radius:8px;background:#2d7dff;color:#fff;text-decoration:none;font-weight:600;">Open in browser</a>
2152
+ </div>
2153
+ <p id="oauthFlowStatus" class="muted" style="margin-top:8px;" aria-live="polite"></p>
2154
+ </div>
2155
+ <label style="margin-top:14px;">Paste redirect URL or authorization code (step 2 → 3)</label>
2156
+ <textarea id="llmOAuthPaste" autocomplete="off" placeholder="After signing in at ChatGPT, paste here: the full http://127.0.0.1:1455/... or http://localhost:1455/... URL from your browser, OR the code string if the prompt asks for the code only."></textarea>
2125
2157
  <label style="display:flex; align-items:flex-start; gap:8px; font-size:13px; color:#9cb0de; margin-top:8px; cursor:pointer;">
2126
2158
  <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>
2159
+ <span>I already completed <code>openclaw models auth login --provider openai-codex</code> in a terminal on <strong>this</strong> machine (skip the buttons above)</span>
2128
2160
  </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>
2161
+ <p class="muted">SSH tip: the browser must reach your machine for localhost callbacks; if that fails, run the same login command in a desktop terminal on this host, then use the checkbox.</p>
2130
2162
  </div>
2131
2163
  <p class="muted" id="llmLoadState" aria-live="polite">Loading LLM provider catalog...</p>
2132
2164
  <div id="llmLoadingHint" class="loading-hint" role="status" aria-live="polite">
@@ -2174,10 +2206,10 @@ function wizardHtml(defaults) {
2174
2206
  </div>
2175
2207
  <div style="margin-top:12px;">
2176
2208
  <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>
2209
+ <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
2210
  </label>
2179
2211
  <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>
2212
+ <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
2213
  </div>
2182
2214
  <button id="start" disabled>Start Installation</button>
2183
2215
  </div>
@@ -2300,6 +2332,51 @@ function wizardHtml(defaults) {
2300
2332
  let pollIntervalMs = 1200;
2301
2333
  let installLocked = false;
2302
2334
  let savedApiKeyProvider = "";
2335
+ let oauthSessionId = null;
2336
+ let oauthWizardLoginDone = false;
2337
+
2338
+ const oauthGetLinkBtn = document.getElementById("oauthGetLinkBtn");
2339
+ const oauthSubmitBtn = document.getElementById("oauthSubmitBtn");
2340
+ const oauthUrlRow = document.getElementById("oauthUrlRow");
2341
+ const oauthUrlDisplay = document.getElementById("oauthUrlDisplay");
2342
+ const oauthCopyUrlBtn = document.getElementById("oauthCopyUrlBtn");
2343
+ const oauthOpenUrlBtn = document.getElementById("oauthOpenUrlBtn");
2344
+ const oauthFlowStatus = document.getElementById("oauthFlowStatus");
2345
+
2346
+ async function cancelOauthSession() {
2347
+ if (!oauthSessionId) {
2348
+ try {
2349
+ await fetch("/api/llm/oauth/cancel", { method: "POST", headers: { "content-type": "application/json" }, body: "{}" });
2350
+ } catch {
2351
+ /* ignore */
2352
+ }
2353
+ return;
2354
+ }
2355
+ try {
2356
+ await fetch("/api/llm/oauth/cancel", {
2357
+ method: "POST",
2358
+ headers: { "content-type": "application/json" },
2359
+ body: JSON.stringify({ sessionId: oauthSessionId }),
2360
+ });
2361
+ } catch {
2362
+ /* ignore */
2363
+ }
2364
+ oauthSessionId = null;
2365
+ }
2366
+
2367
+ function resetOauthWizardState() {
2368
+ oauthSessionId = null;
2369
+ oauthWizardLoginDone = false;
2370
+ if (oauthUrlRow) oauthUrlRow.classList.add("hidden");
2371
+ if (oauthUrlDisplay) oauthUrlDisplay.value = "";
2372
+ if (oauthCopyUrlBtn) oauthCopyUrlBtn.disabled = true;
2373
+ if (oauthOpenUrlBtn) {
2374
+ oauthOpenUrlBtn.href = "#";
2375
+ oauthOpenUrlBtn.setAttribute("aria-disabled", "true");
2376
+ }
2377
+ if (oauthSubmitBtn) oauthSubmitBtn.disabled = true;
2378
+ if (oauthFlowStatus) oauthFlowStatus.textContent = "";
2379
+ }
2303
2380
 
2304
2381
  (function initLlmAuthDefaults() {
2305
2382
  const mode = ${JSON.stringify(defaults.llmAuthMode || "api_key")};
@@ -2332,6 +2409,8 @@ function wizardHtml(defaults) {
2332
2409
  llmApiKeyBlock.classList.add("hidden");
2333
2410
  llmOauthBlock.classList.remove("hidden");
2334
2411
  } else {
2412
+ void cancelOauthSession();
2413
+ resetOauthWizardState();
2335
2414
  llmProviderWrap.classList.remove("hidden");
2336
2415
  llmOauthProviderNote.classList.add("hidden");
2337
2416
  llmApiKeyBlock.classList.remove("hidden");
@@ -2350,6 +2429,7 @@ function wizardHtml(defaults) {
2350
2429
  function hasRequiredInputs() {
2351
2430
  if (!llmCatalogReady || !Boolean(telegramTokenEl.value.trim())) return false;
2352
2431
  if (isOauthMode()) {
2432
+ if (oauthWizardLoginDone) return true;
2353
2433
  if (llmOAuthSkipLoginEl.checked) return true;
2354
2434
  return Boolean(llmOAuthPasteEl.value.trim());
2355
2435
  }
@@ -2542,12 +2622,16 @@ function wizardHtml(defaults) {
2542
2622
  xAccessTokenMain: xAccessTokenMainEl.value.trim(),
2543
2623
  xAccessTokenMainSecret: xAccessTokenMainSecretEl.value.trim(),
2544
2624
  };
2625
+ if (oauth && oauthWizardLoginDone) {
2626
+ payload.llmOAuthSkipLogin = true;
2627
+ payload.llmOAuthPaste = "";
2628
+ }
2545
2629
  if (oauth) {
2546
- if (!payload.llmOAuthSkipLogin && !payload.llmOAuthPaste) {
2630
+ if (!payload.llmOAuthSkipLogin && !payload.llmOAuthPaste && !oauthWizardLoginDone) {
2547
2631
  stateEl.textContent = "blocked";
2548
2632
  readyEl.textContent = "";
2549
2633
  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.";
2634
+ "Codex OAuth: use “Get ChatGPT sign-in link” and “Submit to OpenClaw”, paste a redirect URL/code, or check the box if you already ran openclaw models auth login on this machine.";
2551
2635
  return;
2552
2636
  }
2553
2637
  } else if (!payload.llmProvider || !payload.llmCredential) {
@@ -2800,6 +2884,125 @@ function wizardHtml(defaults) {
2800
2884
  llmCredentialEl.addEventListener("input", updateStartButtonState);
2801
2885
  llmOAuthPasteEl.addEventListener("input", updateStartButtonState);
2802
2886
  llmOAuthSkipLoginEl.addEventListener("change", updateStartButtonState);
2887
+
2888
+ if (oauthGetLinkBtn) {
2889
+ oauthGetLinkBtn.addEventListener("click", async () => {
2890
+ oauthWizardLoginDone = false;
2891
+ if (oauthFlowStatus) {
2892
+ oauthFlowStatus.className = "muted";
2893
+ oauthFlowStatus.textContent = "Starting OpenClaw (same as running models auth login in a terminal)...";
2894
+ }
2895
+ oauthGetLinkBtn.disabled = true;
2896
+ try {
2897
+ const res = await fetch("/api/llm/oauth/start", { method: "POST" });
2898
+ const data = await res.json().catch(() => ({}));
2899
+ if (!res.ok) {
2900
+ if (oauthFlowStatus) {
2901
+ oauthFlowStatus.className = "err-banner";
2902
+ oauthFlowStatus.textContent = data.message || data.error || "Could not get sign-in URL.";
2903
+ }
2904
+ oauthGetLinkBtn.disabled = false;
2905
+ return;
2906
+ }
2907
+ oauthSessionId = data.sessionId;
2908
+ if (oauthUrlDisplay) oauthUrlDisplay.value = data.authUrl || "";
2909
+ if (oauthUrlRow) oauthUrlRow.classList.remove("hidden");
2910
+ if (oauthCopyUrlBtn) oauthCopyUrlBtn.disabled = !data.authUrl;
2911
+ if (oauthOpenUrlBtn && data.authUrl) {
2912
+ oauthOpenUrlBtn.href = data.authUrl;
2913
+ oauthOpenUrlBtn.removeAttribute("aria-disabled");
2914
+ }
2915
+ if (oauthSubmitBtn) oauthSubmitBtn.disabled = false;
2916
+ if (oauthFlowStatus) {
2917
+ oauthFlowStatus.className = "muted";
2918
+ oauthFlowStatus.textContent =
2919
+ "Open this URL in your browser (on your PC if you use SSH). After ChatGPT sign-in, paste the localhost redirect URL or the code, then click Submit.";
2920
+ }
2921
+ } catch (err) {
2922
+ if (oauthFlowStatus) {
2923
+ oauthFlowStatus.className = "err-banner";
2924
+ oauthFlowStatus.textContent = err && err.message ? String(err.message) : "Request failed.";
2925
+ }
2926
+ }
2927
+ oauthGetLinkBtn.disabled = false;
2928
+ });
2929
+ }
2930
+
2931
+ if (oauthSubmitBtn) {
2932
+ oauthSubmitBtn.addEventListener("click", async () => {
2933
+ const paste = llmOAuthPasteEl.value.trim();
2934
+ if (!paste) {
2935
+ if (oauthFlowStatus) {
2936
+ oauthFlowStatus.className = "err-banner";
2937
+ oauthFlowStatus.textContent = "Paste the redirect URL (from the browser address bar) or the authorization code first.";
2938
+ }
2939
+ return;
2940
+ }
2941
+ if (!oauthSessionId) {
2942
+ if (oauthFlowStatus) {
2943
+ oauthFlowStatus.className = "err-banner";
2944
+ oauthFlowStatus.textContent = "Click “Get ChatGPT sign-in link” first, or use the “already completed login in terminal” checkbox.";
2945
+ }
2946
+ return;
2947
+ }
2948
+ oauthSubmitBtn.disabled = true;
2949
+ if (oauthFlowStatus) {
2950
+ oauthFlowStatus.className = "muted";
2951
+ oauthFlowStatus.textContent = "Sending to OpenClaw...";
2952
+ }
2953
+ try {
2954
+ const res = await fetch("/api/llm/oauth/submit", {
2955
+ method: "POST",
2956
+ headers: { "content-type": "application/json" },
2957
+ body: JSON.stringify({ sessionId: oauthSessionId, paste }),
2958
+ });
2959
+ const data = await res.json().catch(() => ({}));
2960
+ if (!res.ok) {
2961
+ if (oauthFlowStatus) {
2962
+ oauthFlowStatus.className = "err-banner";
2963
+ oauthFlowStatus.textContent = data.message || data.error || "Login failed.";
2964
+ }
2965
+ oauthSubmitBtn.disabled = false;
2966
+ return;
2967
+ }
2968
+ oauthWizardLoginDone = true;
2969
+ oauthSessionId = null;
2970
+ llmOAuthSkipLoginEl.checked = true;
2971
+ if (oauthFlowStatus) {
2972
+ oauthFlowStatus.className = "ok-banner";
2973
+ oauthFlowStatus.textContent = "OpenClaw saved your ChatGPT login. You can start installation below.";
2974
+ }
2975
+ oauthSubmitBtn.disabled = true;
2976
+ updateStartButtonState();
2977
+ } catch (err) {
2978
+ if (oauthFlowStatus) {
2979
+ oauthFlowStatus.className = "err-banner";
2980
+ oauthFlowStatus.textContent = err && err.message ? String(err.message) : "Request failed.";
2981
+ }
2982
+ oauthSubmitBtn.disabled = false;
2983
+ }
2984
+ });
2985
+ }
2986
+
2987
+ if (oauthCopyUrlBtn && oauthUrlDisplay) {
2988
+ oauthCopyUrlBtn.addEventListener("click", async () => {
2989
+ const v = oauthUrlDisplay.value;
2990
+ if (!v) return;
2991
+ try {
2992
+ await navigator.clipboard.writeText(v);
2993
+ oauthCopyUrlBtn.textContent = "Copied";
2994
+ setTimeout(() => {
2995
+ oauthCopyUrlBtn.textContent = "Copy URL";
2996
+ }, 1500);
2997
+ } catch {
2998
+ oauthCopyUrlBtn.textContent = "Copy failed";
2999
+ setTimeout(() => {
3000
+ oauthCopyUrlBtn.textContent = "Copy URL";
3001
+ }, 1500);
3002
+ }
3003
+ });
3004
+ }
3005
+
2803
3006
  telegramTokenEl.addEventListener("input", updateStartButtonState);
2804
3007
  xConsumerKeyEl.addEventListener("input", updateStartButtonState);
2805
3008
  xConsumerSecretEl.addEventListener("input", updateStartButtonState);
@@ -2841,6 +3044,31 @@ async function cmdInstall(args) {
2841
3044
  let running = false;
2842
3045
  let shuttingDown = false;
2843
3046
 
3047
+ /** In-browser Codex OAuth: one pending `openclaw models auth login` child waiting for stdin (same flow as CLI). */
3048
+ const oauthSessions = new Map();
3049
+ const OPENAI_OAUTH_AUTHORIZE_RE = /https:\/\/auth\.openai\.com\/oauth\/authorize\S*/;
3050
+ const oauthSessionTtlMs = 15 * 60 * 1000;
3051
+
3052
+ function killOauthSession(sessionId, signal = "SIGTERM") {
3053
+ const s = oauthSessions.get(sessionId);
3054
+ if (!s) return;
3055
+ try {
3056
+ s.child.kill(signal);
3057
+ } catch {
3058
+ /* ignore */
3059
+ }
3060
+ oauthSessions.delete(sessionId);
3061
+ }
3062
+
3063
+ function pruneExpiredOauthSessions() {
3064
+ const now = Date.now();
3065
+ for (const [id, s] of oauthSessions) {
3066
+ if (now - s.createdAt > oauthSessionTtlMs) {
3067
+ killOauthSession(id);
3068
+ }
3069
+ }
3070
+ }
3071
+
2844
3072
  const server = createServer(async (req, res) => {
2845
3073
  const respondJson = (code, payload) => {
2846
3074
  res.statusCode = code;
@@ -2904,6 +3132,169 @@ async function cmdInstall(args) {
2904
3132
  return;
2905
3133
  }
2906
3134
 
3135
+ if (req.method === "POST" && req.url === "/api/llm/oauth/start") {
3136
+ pruneExpiredOauthSessions();
3137
+ if (running) {
3138
+ respondJson(409, { ok: false, error: "install_in_progress" });
3139
+ return;
3140
+ }
3141
+ if (!commandExists("openclaw")) {
3142
+ respondJson(503, {
3143
+ ok: false,
3144
+ error: "openclaw_not_in_path",
3145
+ message: "Install OpenClaw on this machine first (e.g. npm install -g openclaw), then try again.",
3146
+ });
3147
+ return;
3148
+ }
3149
+ for (const id of [...oauthSessions.keys()]) {
3150
+ killOauthSession(id);
3151
+ }
3152
+
3153
+ const sessionId = randomUUID();
3154
+ const child = spawn("openclaw", ["models", "auth", "login", "--provider", "openai-codex"], {
3155
+ stdio: ["pipe", "pipe", "pipe"],
3156
+ shell: false,
3157
+ });
3158
+
3159
+ let combined = "";
3160
+ let responded = false;
3161
+ const urlTimeout = setTimeout(() => {
3162
+ if (responded) return;
3163
+ responded = true;
3164
+ try {
3165
+ child.kill("SIGTERM");
3166
+ } catch {
3167
+ /* ignore */
3168
+ }
3169
+ oauthSessions.delete(sessionId);
3170
+ respondJson(504, {
3171
+ ok: false,
3172
+ error: "oauth_url_timeout",
3173
+ message:
3174
+ "OpenClaw did not print a ChatGPT sign-in URL in time. Run `openclaw models auth login --provider openai-codex` in a terminal on this machine, then use the checkbox below.",
3175
+ });
3176
+ }, 45_000);
3177
+
3178
+ const trySendUrl = () => {
3179
+ if (responded) return;
3180
+ const m = combined.match(OPENAI_OAUTH_AUTHORIZE_RE);
3181
+ if (!m || !m[0]) return;
3182
+ clearTimeout(urlTimeout);
3183
+ responded = true;
3184
+ oauthSessions.set(sessionId, { child, createdAt: Date.now(), submitted: false });
3185
+ respondJson(200, { ok: true, sessionId, authUrl: m[0] });
3186
+ };
3187
+
3188
+ child.stdout?.on("data", (d) => {
3189
+ combined += d.toString();
3190
+ trySendUrl();
3191
+ });
3192
+ child.stderr?.on("data", (d) => {
3193
+ combined += d.toString();
3194
+ trySendUrl();
3195
+ });
3196
+ child.on("error", (err) => {
3197
+ clearTimeout(urlTimeout);
3198
+ if (responded) return;
3199
+ responded = true;
3200
+ oauthSessions.delete(sessionId);
3201
+ respondJson(500, { ok: false, error: "spawn_failed", message: err.message });
3202
+ });
3203
+ child.on("close", (code) => {
3204
+ clearTimeout(urlTimeout);
3205
+ if (!responded) {
3206
+ responded = true;
3207
+ oauthSessions.delete(sessionId);
3208
+ respondJson(500, {
3209
+ ok: false,
3210
+ error: "oauth_login_exited_early",
3211
+ exitCode: code,
3212
+ detail: combined.slice(-4000),
3213
+ });
3214
+ return;
3215
+ }
3216
+ const pending = oauthSessions.get(sessionId);
3217
+ if (pending && !pending.submitted) {
3218
+ oauthSessions.delete(sessionId);
3219
+ }
3220
+ });
3221
+ return;
3222
+ }
3223
+
3224
+ if (req.method === "POST" && req.url === "/api/llm/oauth/submit") {
3225
+ const body = await parseJsonBody(req).catch(() => ({}));
3226
+ const sessionId = typeof body.sessionId === "string" ? body.sessionId : "";
3227
+ const paste = typeof body.paste === "string" ? body.paste.trim() : "";
3228
+ if (!sessionId || !oauthSessions.has(sessionId)) {
3229
+ respondJson(400, { ok: false, error: "invalid_or_expired_session", message: "Click “Get ChatGPT sign-in link” again." });
3230
+ return;
3231
+ }
3232
+ if (!paste) {
3233
+ respondJson(400, { ok: false, error: "paste_required" });
3234
+ return;
3235
+ }
3236
+ const s = oauthSessions.get(sessionId);
3237
+ s.submitted = true;
3238
+ const { child } = s;
3239
+
3240
+ await new Promise((resolve) => {
3241
+ let settled = false;
3242
+ let submitTimeout;
3243
+ const finish = (httpCode, payload) => {
3244
+ if (settled) return;
3245
+ settled = true;
3246
+ clearTimeout(submitTimeout);
3247
+ oauthSessions.delete(sessionId);
3248
+ respondJson(httpCode, payload);
3249
+ resolve();
3250
+ };
3251
+
3252
+ submitTimeout = setTimeout(() => {
3253
+ killOauthSession(sessionId);
3254
+ finish(504, {
3255
+ ok: false,
3256
+ error: "oauth_submit_timeout",
3257
+ message: "Login did not finish in time. Try again or complete login in a normal terminal.",
3258
+ });
3259
+ }, 120_000);
3260
+
3261
+ child.once("close", (code) => {
3262
+ if (code === 0) {
3263
+ finish(200, { ok: true });
3264
+ } else {
3265
+ finish(500, {
3266
+ ok: false,
3267
+ error: "oauth_login_failed",
3268
+ exitCode: code,
3269
+ message: "OpenClaw rejected the pasted URL or code. Paste the full redirect URL from the browser address bar, or run login in a terminal.",
3270
+ });
3271
+ }
3272
+ });
3273
+
3274
+ try {
3275
+ child.stdin.write(`${paste}\n`);
3276
+ } catch (err) {
3277
+ killOauthSession(sessionId);
3278
+ finish(500, { ok: false, error: "stdin_write_failed", message: err?.message || String(err) });
3279
+ }
3280
+ });
3281
+ return;
3282
+ }
3283
+
3284
+ if (req.method === "POST" && req.url === "/api/llm/oauth/cancel") {
3285
+ const body = await parseJsonBody(req).catch(() => ({}));
3286
+ const sessionId = typeof body.sessionId === "string" ? body.sessionId : "";
3287
+ if (sessionId && oauthSessions.has(sessionId)) {
3288
+ killOauthSession(sessionId);
3289
+ } else {
3290
+ for (const id of [...oauthSessions.keys()]) {
3291
+ killOauthSession(id);
3292
+ }
3293
+ }
3294
+ respondJson(200, { ok: true });
3295
+ return;
3296
+ }
3297
+
2907
3298
  if (req.method === "POST" && req.url === "/api/finish") {
2908
3299
  if (running || runtime.status !== "completed") {
2909
3300
  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.75",
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.75"
21
21
  },
22
22
  "keywords": [
23
23
  "traderclaw",