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.
- package/bin/installer-step-engine.mjs +26 -4
- package/bin/openclaw-trader.mjs +474 -11
- package/package.json +2 -2
|
@@ -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 =
|
|
1604
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
1605
|
-
shell: false,
|
|
1606
|
-
});
|
|
1628
|
+
const child = spawnOpenClawCodexAuthLoginChild();
|
|
1607
1629
|
|
|
1608
1630
|
let stdout = "";
|
|
1609
1631
|
let stderr = "";
|
package/bin/openclaw-trader.mjs
CHANGED
|
@@ -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,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
|
-
<
|
|
2124
|
-
|
|
2125
|
-
|
|
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>
|
|
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
|
-
<
|
|
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 +
|
|
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">
|
|
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:
|
|
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 } =
|
|
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.
|
|
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.
|
|
20
|
+
"solana-traderclaw": "^1.0.76"
|
|
21
21
|
},
|
|
22
22
|
"keywords": [
|
|
23
23
|
"traderclaw",
|