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.
- package/bin/openclaw-trader.mjs +401 -10
- package/package.json +2 -2
package/bin/openclaw-trader.mjs
CHANGED
|
@@ -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 +
|
|
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
|
-
<
|
|
2124
|
-
|
|
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
|
|
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">
|
|
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 +
|
|
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">
|
|
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:
|
|
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.
|
|
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.
|
|
20
|
+
"solana-traderclaw": "^1.0.75"
|
|
21
21
|
},
|
|
22
22
|
"keywords": [
|
|
23
23
|
"traderclaw",
|