traderclaw-cli 1.0.73 → 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.
@@ -1562,6 +1562,115 @@ function configureOpenClawLlmProvider({ provider, model, credential }, configPat
1562
1562
  return { configPath, provider, model };
1563
1563
  }
1564
1564
 
1565
+ /**
1566
+ * Sets only `agents.defaults.model.primary` (OAuth / subscription paths where credentials live in OpenClaw auth profiles).
1567
+ * Does not write API keys into config.env.
1568
+ */
1569
+ function configureOpenClawLlmModelPrimaryOnly({ provider, model }, configPath = CONFIG_FILE) {
1570
+ if (!provider || !model) {
1571
+ throw new Error("LLM provider and model are required.");
1572
+ }
1573
+ if (!model.startsWith(`${provider}/`)) {
1574
+ throw new Error(`Selected model '${model}' does not match provider '${provider}'.`);
1575
+ }
1576
+
1577
+ let config = {};
1578
+ try {
1579
+ config = JSON.parse(readFileSync(configPath, "utf-8"));
1580
+ } catch {
1581
+ config = {};
1582
+ }
1583
+
1584
+ if (!config.agents) config.agents = {};
1585
+ if (!config.agents.defaults) config.agents.defaults = {};
1586
+ ensureAgentsDefaultsSchemaCompat(config);
1587
+ if (!config.agents.defaults.model || typeof config.agents.defaults.model !== "object") {
1588
+ config.agents.defaults.model = {};
1589
+ }
1590
+ config.agents.defaults.model.primary = model;
1591
+
1592
+ mkdirSync(CONFIG_DIR, { recursive: true });
1593
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1594
+ return { configPath, provider, model };
1595
+ }
1596
+
1597
+ /**
1598
+ * Runs `openclaw models auth login --provider openai-codex` and feeds the pasted redirect URL or code on stdin
1599
+ * when the CLI prompts (with a timed fallback for non-interactive / SSH).
1600
+ */
1601
+ function runOpenClawCodexOAuthLogin(paste, emitLog) {
1602
+ 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
+ });
1607
+
1608
+ let stdout = "";
1609
+ let stderr = "";
1610
+ let pasteSent = false;
1611
+
1612
+ const sendPaste = () => {
1613
+ if (pasteSent) return;
1614
+ const p = String(paste || "").trim();
1615
+ if (!p) return;
1616
+ pasteSent = true;
1617
+ try {
1618
+ child.stdin.write(`${p}\n`);
1619
+ } catch {
1620
+ // ignore
1621
+ }
1622
+ };
1623
+
1624
+ let fallbackTimer = setTimeout(() => sendPaste(), 9000);
1625
+
1626
+ const onChunk = (chunk) => {
1627
+ const combined = (stdout + stderr).toLowerCase();
1628
+ const c = typeof chunk === "string" ? chunk : chunk.toString();
1629
+ if (!pasteSent && /paste|authorization|redirect|callback/i.test(combined) && c.length > 0) {
1630
+ clearTimeout(fallbackTimer);
1631
+ fallbackTimer = setTimeout(() => sendPaste(), 400);
1632
+ }
1633
+ };
1634
+
1635
+ child.stdout?.on("data", (d) => {
1636
+ const t = d.toString();
1637
+ stdout += t;
1638
+ const urls = extractUrls(t);
1639
+ emitLog("info", t, urls);
1640
+ onChunk(t);
1641
+ });
1642
+
1643
+ child.stderr?.on("data", (d) => {
1644
+ const t = d.toString();
1645
+ stderr += t;
1646
+ const urls = extractUrls(t);
1647
+ emitLog("warn", t, urls);
1648
+ onChunk(t);
1649
+ });
1650
+
1651
+ child.on("close", (code) => {
1652
+ clearTimeout(fallbackTimer);
1653
+ if (code === 0) {
1654
+ resolve({ stdout, stderr });
1655
+ return;
1656
+ }
1657
+ const detail = `${stderr}\n${stdout}`.trim();
1658
+ const err = new Error(
1659
+ detail || `openclaw models auth login failed with exit code ${code}. Try running the same command in a normal shell, then re-run the wizard with "already logged in" checked.`,
1660
+ );
1661
+ err.code = code;
1662
+ err.stdout = stdout;
1663
+ err.stderr = stderr;
1664
+ reject(err);
1665
+ });
1666
+
1667
+ child.on("error", (e) => {
1668
+ clearTimeout(fallbackTimer);
1669
+ reject(e);
1670
+ });
1671
+ });
1672
+ }
1673
+
1565
1674
  function verifyInstallation(modeConfig, apiKey) {
1566
1675
  const gatewayFile = join(CONFIG_DIR, "gateway", modeConfig.gatewayConfig);
1567
1676
  let llmConfigured = false;
@@ -1645,9 +1754,12 @@ export class InstallerStepEngine {
1645
1754
  this.modeConfig = modeConfig;
1646
1755
  this.options = {
1647
1756
  lane: normalizeLane(options.lane),
1757
+ llmAuthMode: options.llmAuthMode === "oauth" ? "oauth" : "api_key",
1648
1758
  llmProvider: options.llmProvider || "",
1649
1759
  llmModel: options.llmModel || "",
1650
1760
  llmCredential: options.llmCredential || "",
1761
+ llmOAuthPaste: typeof options.llmOAuthPaste === "string" ? options.llmOAuthPaste.trim() : "",
1762
+ llmOAuthSkipLogin: options.llmOAuthSkipLogin === true,
1651
1763
  apiKey: options.apiKey || "",
1652
1764
  orchestratorUrl: options.orchestratorUrl || "https://api.traderclaw.ai",
1653
1765
  gatewayBaseUrl: options.gatewayBaseUrl || "",
@@ -1924,15 +2036,78 @@ export class InstallerStepEngine {
1924
2036
  const provider = String(this.options.llmProvider || "").trim();
1925
2037
  const requestedModel = String(this.options.llmModel || "").trim();
1926
2038
  const credential = String(this.options.llmCredential || "").trim();
1927
- if (!provider || !credential) {
2039
+ const authMode = this.options.llmAuthMode === "oauth" ? "oauth" : "api_key";
2040
+
2041
+ if (!provider) {
1928
2042
  throw new Error(
1929
- "Missing required LLM settings. Select provider and provide credential in the wizard before starting installation.",
2043
+ "Missing required LLM settings. Select provider in the wizard before starting installation.",
1930
2044
  );
1931
2045
  }
1932
2046
  if (!commandExists("openclaw")) {
1933
2047
  throw new Error("OpenClaw is not available yet. Install step must complete before LLM configuration.");
1934
2048
  }
1935
2049
 
2050
+ if (authMode === "oauth") {
2051
+ if (provider !== "openai-codex") {
2052
+ throw new Error("OAuth mode requires LLM provider openai-codex (ChatGPT / Codex subscription).");
2053
+ }
2054
+ const skipLogin = this.options.llmOAuthSkipLogin === true;
2055
+ const oauthPaste = String(this.options.llmOAuthPaste || "").trim();
2056
+ if (!skipLogin && !oauthPaste) {
2057
+ throw new Error(
2058
+ "Codex OAuth requires a pasted authorization code or redirect URL, or enable skip if you already ran openclaw models auth login on this host.",
2059
+ );
2060
+ }
2061
+ if (!skipLogin) {
2062
+ try {
2063
+ await runOpenClawCodexOAuthLogin(oauthPaste, (level, text, urls) =>
2064
+ this.emitLog("configure_llm", level, text, urls || []),
2065
+ );
2066
+ } catch (err) {
2067
+ const tail = `${err?.stderr || ""}\n${err?.stdout || ""}\n${err?.message || ""}`.trim();
2068
+ throw new Error(
2069
+ `${tail}\n\nIf OAuth cannot complete from the wizard, run in a shell: openclaw models auth login --provider openai-codex — then re-run the wizard with "already logged in" checked.`,
2070
+ );
2071
+ }
2072
+ }
2073
+
2074
+ const selection = resolveLlmModelSelection(provider, requestedModel);
2075
+ for (const msg of selection.warnings) {
2076
+ this.emitLog("configure_llm", "warn", msg);
2077
+ }
2078
+ const model = selection.model;
2079
+
2080
+ const saved = configureOpenClawLlmModelPrimaryOnly({ provider, model });
2081
+ this.emitLog(
2082
+ "configure_llm",
2083
+ "info",
2084
+ `Configured OpenClaw model primary=${model} (Codex OAuth; credentials in OpenClaw auth profiles, not OPENAI_API_KEY).`,
2085
+ );
2086
+
2087
+ await runCommandWithEvents("openclaw", ["config", "validate"], {
2088
+ onEvent: (evt) => this.emitLog("configure_llm", evt.type === "stderr" ? "warn" : "info", evt.text, evt.urls || []),
2089
+ });
2090
+
2091
+ try {
2092
+ await runCommandWithEvents("openclaw", ["models", "status", "--check", "--probe-provider", provider], {
2093
+ onEvent: (evt) => this.emitLog("configure_llm", evt.type === "stderr" ? "warn" : "info", evt.text, evt.urls || []),
2094
+ });
2095
+ } catch (err) {
2096
+ const details = `${err?.stderr || ""}\n${err?.stdout || ""}\n${err?.message || ""}`.trim();
2097
+ throw new Error(
2098
+ `LLM provider validation failed for '${provider}'. Check OAuth login and model, then retry.\n${details}`,
2099
+ );
2100
+ }
2101
+
2102
+ return { configured: true, provider, model, configPath: saved.configPath, authMode: "oauth" };
2103
+ }
2104
+
2105
+ if (!credential) {
2106
+ throw new Error(
2107
+ "Missing required LLM settings. Paste your API key or token in the wizard before starting installation.",
2108
+ );
2109
+ }
2110
+
1936
2111
  const selection = resolveLlmModelSelection(provider, requestedModel);
1937
2112
  for (const msg of selection.warnings) {
1938
2113
  this.emitLog("configure_llm", "warn", msg);
@@ -1957,7 +2132,7 @@ export class InstallerStepEngine {
1957
2132
  );
1958
2133
  }
1959
2134
 
1960
- return { configured: true, provider, model, configPath: saved.configPath };
2135
+ return { configured: true, provider, model, configPath: saved.configPath, authMode: "api_key" };
1961
2136
  }
1962
2137
 
1963
2138
  buildSetupHandoff() {
@@ -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
  }
@@ -1689,6 +1689,9 @@ function parseInstallWizardArgs(args) {
1689
1689
  llmProvider: "",
1690
1690
  llmModel: "",
1691
1691
  llmCredential: "",
1692
+ llmAuthMode: "api_key",
1693
+ llmOAuthPaste: "",
1694
+ llmOAuthSkipLogin: false,
1692
1695
  orchestratorUrl: "https://api.traderclaw.ai",
1693
1696
  gatewayBaseUrl: "",
1694
1697
  gatewayToken: "",
@@ -1709,6 +1712,12 @@ function parseInstallWizardArgs(args) {
1709
1712
  if (key === "--llm-provider" && next) out.llmProvider = args[++i];
1710
1713
  if (key === "--llm-model" && next) out.llmModel = args[++i];
1711
1714
  if ((key === "--llm-api-key" || key === "--llm-token") && next) out.llmCredential = args[++i];
1715
+ if (key === "--llm-auth" && next) {
1716
+ const v = args[++i];
1717
+ out.llmAuthMode = v === "oauth" ? "oauth" : "api_key";
1718
+ }
1719
+ if (key === "--llm-oauth-paste" && next) out.llmOAuthPaste = args[++i];
1720
+ if (key === "--llm-oauth-skip-login") out.llmOAuthSkipLogin = true;
1712
1721
  if ((key === "--url" || key === "-u") && next) out.orchestratorUrl = args[++i];
1713
1722
  if ((key === "--gateway-base-url" || key === "-g") && next) out.gatewayBaseUrl = args[++i];
1714
1723
  if ((key === "--gateway-token" || key === "-t") && next) out.gatewayToken = args[++i];
@@ -1867,7 +1876,7 @@ async function cmdPrecheck(args) {
1867
1876
 
1868
1877
  log.info("Manual staging run commands:");
1869
1878
  log.info(" 1) traderclaw install --wizard");
1870
- log.info(" 2) In wizard, set LLM provider + credential and Telegram token");
1879
+ log.info(" 2) In wizard, set LLM (API key or Codex OAuth) and Telegram token");
1871
1880
  log.info(" 3) Approve tailscale login in provided URL");
1872
1881
  log.info(" 4) Confirm /v1/responses returns non-404 on funnel host");
1873
1882
  log.info(" 5) Verify Telegram channel setup + probe");
@@ -1944,7 +1953,13 @@ async function loadWizardLlmCatalogAsync() {
1944
1953
  },
1945
1954
  {
1946
1955
  id: "openai-codex",
1947
- models: [{ id: "openai-codex/gpt-5-codex", name: "GPT-5 Codex" }],
1956
+ models: [
1957
+ { id: "openai-codex/gpt-5.4", name: "GPT-5.4 Codex (recommended, ChatGPT OAuth)" },
1958
+ {
1959
+ id: "openai-codex/gpt-5.3-codex-spark",
1960
+ name: "GPT-5.3 Codex Spark (subscription entitlement; experimental)",
1961
+ },
1962
+ ],
1948
1963
  },
1949
1964
  {
1950
1965
  id: "google",
@@ -2027,7 +2042,8 @@ function wizardHtml(defaults) {
2027
2042
  .card { background:#121a31; border:1px solid #22315a; border-radius: 12px; padding: 16px; margin-bottom: 16px; }
2028
2043
  .grid { display:grid; grid-template-columns:1fr 1fr; gap: 12px; }
2029
2044
  label { display:block; font-size: 12px; color:#9cb0de; margin-bottom: 4px; }
2030
- input, select { width:100%; padding:10px; border-radius:8px; border:1px solid #334a87; background:#0d1530; color:#e8eef9; }
2045
+ input, select, textarea { width:100%; padding:10px; border-radius:8px; border:1px solid #334a87; background:#0d1530; color:#e8eef9; font-family: inherit; font-size: 14px; box-sizing: border-box; }
2046
+ textarea { min-height: 88px; resize: vertical; }
2031
2047
  button { border:0; border-radius:8px; padding:10px 14px; background:#4d7cff; color:#fff; cursor:pointer; font-weight:600; }
2032
2048
  button:disabled { opacity:0.6; cursor:not-allowed; }
2033
2049
  .muted { color:#9cb0de; font-size:13px; }
@@ -2059,6 +2075,15 @@ function wizardHtml(defaults) {
2059
2075
  .muted a:hover { color:#c5e5ff; }
2060
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; }
2061
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; }
2062
2087
  </style>
2063
2088
  </head>
2064
2089
  <body>
@@ -2069,9 +2094,20 @@ function wizardHtml(defaults) {
2069
2094
  </div>
2070
2095
  <div class="card" id="llmCard">
2071
2096
  <h3>Required: OpenClaw LLM Provider</h3>
2072
- <p class="muted">Pick your LLM provider and paste your credential. Beginner mode supports common API-key providers.</p>
2073
- <div class="grid">
2074
- <div>
2097
+ <p class="muted">Use an API key for OpenAI Platform and other providers, or ChatGPT/Codex OAuth for subscription access (no separate API billing). OAuth tokens are stored by OpenClaw — not as <code>OPENAI_API_KEY</code>.</p>
2098
+ <div style="margin-bottom:12px;">
2099
+ <label style="margin-bottom:8px;">LLM authentication</label>
2100
+ <label style="display:flex; align-items:flex-start; gap:8px; font-size:14px; color:#e8eef9; margin-bottom:6px; cursor:pointer;">
2101
+ <input id="llmAuthModeApiKey" name="llmAuthMode" type="radio" value="api_key" style="width:auto; margin-top:3px;" checked />
2102
+ <span><strong>API key</strong> — OpenAI Platform (<code>openai</code>), Anthropic, OpenRouter, etc.</span>
2103
+ </label>
2104
+ <label style="display:flex; align-items:flex-start; gap:8px; font-size:14px; color:#e8eef9; cursor:pointer;">
2105
+ <input id="llmAuthModeOauth" name="llmAuthMode" type="radio" value="oauth" style="width:auto; margin-top:3px;" />
2106
+ <span><strong>ChatGPT / Codex (OAuth)</strong> — provider <code>openai-codex</code> (Plus/Pro subscription via OpenClaw login)</span>
2107
+ </label>
2108
+ </div>
2109
+ <div class="grid" id="llmProviderModelGrid">
2110
+ <div id="llmProviderWrap">
2075
2111
  <label>LLM provider (required)</label>
2076
2112
  <select id="llmProvider"></select>
2077
2113
  </div>
@@ -2080,21 +2116,54 @@ function wizardHtml(defaults) {
2080
2116
  <select id="llmModel"></select>
2081
2117
  </div>
2082
2118
  </div>
2119
+ <p class="muted hidden" id="llmOauthProviderNote">Provider is fixed to <code>openai-codex</code>. Pick a Codex model below (or enable manual selection).</p>
2083
2120
  <div style="margin-top:8px;">
2084
2121
  <label style="display:flex; align-items:center; gap:8px; font-size:13px; color:#9cb0de;">
2085
2122
  <input id="llmModelManual" type="checkbox" style="width:auto; padding:0; margin:0;" />
2086
2123
  Choose model manually (advanced)
2087
2124
  </label>
2088
2125
  </div>
2089
- <div style="margin-top:12px;">
2126
+ <div style="margin-top:12px;" id="llmApiKeyBlock">
2090
2127
  <label>LLM API key or token (required)</label>
2091
2128
  <input id="llmCredential" type="password" placeholder="Paste the credential for the selected provider/model" />
2092
- <p class="muted">This credential is written to OpenClaw model provider config so your agent can run. If you skip manual model selection, the installer will choose a safe provider default.</p>
2093
- <p class="muted" id="llmLoadState" aria-live="polite">Loading LLM provider catalog...</p>
2094
- <div id="llmLoadingHint" class="loading-hint" role="status" aria-live="polite">
2095
- <span class="spinner" aria-hidden="true"></span>
2096
- <span id="llmLoadingHintText">Fetching provider list...</span>
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>
2130
+ </div>
2131
+ <div style="margin-top:12px;" id="llmOauthBlock" class="hidden">
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>
2097
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>
2157
+ <label style="display:flex; align-items:flex-start; gap:8px; font-size:13px; color:#9cb0de; margin-top:8px; cursor:pointer;">
2158
+ <input id="llmOAuthSkipLogin" type="checkbox" style="width:auto; margin-top:3px;" />
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>
2160
+ </label>
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>
2162
+ </div>
2163
+ <p class="muted" id="llmLoadState" aria-live="polite">Loading LLM provider catalog...</p>
2164
+ <div id="llmLoadingHint" class="loading-hint" role="status" aria-live="polite">
2165
+ <span class="spinner" aria-hidden="true"></span>
2166
+ <span id="llmLoadingHintText">Fetching provider list...</span>
2098
2167
  </div>
2099
2168
  </div>
2100
2169
  <div class="card" id="xCard">
@@ -2137,10 +2206,10 @@ function wizardHtml(defaults) {
2137
2206
  </div>
2138
2207
  <div style="margin-top:12px;">
2139
2208
  <label style="display:flex;align-items:center;gap:8px;">Referral code (optional)
2140
- <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>
2141
2210
  </label>
2142
2211
  <input id="referralCode" type="text" maxlength="16" autocomplete="off" placeholder="e.g. ABCD1234" />
2143
- <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>
2144
2213
  </div>
2145
2214
  <button id="start" disabled>Start Installation</button>
2146
2215
  </div>
@@ -2228,6 +2297,14 @@ function wizardHtml(defaults) {
2228
2297
  const llmModelEl = document.getElementById("llmModel");
2229
2298
  const llmModelManualEl = document.getElementById("llmModelManual");
2230
2299
  const llmCredentialEl = document.getElementById("llmCredential");
2300
+ const llmAuthModeApiKey = document.getElementById("llmAuthModeApiKey");
2301
+ const llmAuthModeOauth = document.getElementById("llmAuthModeOauth");
2302
+ const llmProviderWrap = document.getElementById("llmProviderWrap");
2303
+ const llmOauthProviderNote = document.getElementById("llmOauthProviderNote");
2304
+ const llmApiKeyBlock = document.getElementById("llmApiKeyBlock");
2305
+ const llmOauthBlock = document.getElementById("llmOauthBlock");
2306
+ const llmOAuthPasteEl = document.getElementById("llmOAuthPaste");
2307
+ const llmOAuthSkipLoginEl = document.getElementById("llmOAuthSkipLogin");
2231
2308
  const telegramTokenEl = document.getElementById("telegramToken");
2232
2309
  const llmLoadStateEl = document.getElementById("llmLoadState");
2233
2310
  const llmLoadingHintEl = document.getElementById("llmLoadingHint");
@@ -2254,14 +2331,109 @@ function wizardHtml(defaults) {
2254
2331
  let pollTimer = null;
2255
2332
  let pollIntervalMs = 1200;
2256
2333
  let installLocked = false;
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
+ }
2380
+
2381
+ (function initLlmAuthDefaults() {
2382
+ const mode = ${JSON.stringify(defaults.llmAuthMode || "api_key")};
2383
+ if (mode === "oauth") {
2384
+ llmAuthModeOauth.checked = true;
2385
+ llmAuthModeApiKey.checked = false;
2386
+ }
2387
+ llmOAuthPasteEl.value = ${JSON.stringify(defaults.llmOAuthPaste || "")};
2388
+ if (${defaults.llmOAuthSkipLogin === true ? "true" : "false"}) {
2389
+ llmOAuthSkipLoginEl.checked = true;
2390
+ }
2391
+ applyLlmAuthModeUi();
2392
+ })();
2393
+
2394
+ function isOauthMode() {
2395
+ return llmAuthModeOauth && llmAuthModeOauth.checked;
2396
+ }
2397
+
2398
+ function effectiveLlmProvider() {
2399
+ return isOauthMode() ? "openai-codex" : llmProviderEl.value.trim();
2400
+ }
2401
+
2402
+ function applyLlmAuthModeUi() {
2403
+ if (!llmProviderWrap || !llmApiKeyBlock || !llmOauthBlock) return;
2404
+ if (isOauthMode()) {
2405
+ savedApiKeyProvider = llmProviderEl.value || savedApiKeyProvider;
2406
+ llmProviderEl.value = "openai-codex";
2407
+ llmProviderWrap.classList.add("hidden");
2408
+ llmOauthProviderNote.classList.remove("hidden");
2409
+ llmApiKeyBlock.classList.add("hidden");
2410
+ llmOauthBlock.classList.remove("hidden");
2411
+ } else {
2412
+ void cancelOauthSession();
2413
+ resetOauthWizardState();
2414
+ llmProviderWrap.classList.remove("hidden");
2415
+ llmOauthProviderNote.classList.add("hidden");
2416
+ llmApiKeyBlock.classList.remove("hidden");
2417
+ llmOauthBlock.classList.add("hidden");
2418
+ if (savedApiKeyProvider) {
2419
+ llmProviderEl.value = savedApiKeyProvider;
2420
+ }
2421
+ }
2422
+ }
2423
+
2424
+ function onLlmAuthModeChange() {
2425
+ applyLlmAuthModeUi();
2426
+ refreshModelOptions("");
2427
+ }
2257
2428
 
2258
2429
  function hasRequiredInputs() {
2259
- return (
2260
- llmCatalogReady
2261
- && Boolean(llmProviderEl.value.trim())
2262
- && Boolean(llmCredentialEl.value.trim())
2263
- && Boolean(telegramTokenEl.value.trim())
2264
- );
2430
+ if (!llmCatalogReady || !Boolean(telegramTokenEl.value.trim())) return false;
2431
+ if (isOauthMode()) {
2432
+ if (oauthWizardLoginDone) return true;
2433
+ if (llmOAuthSkipLoginEl.checked) return true;
2434
+ return Boolean(llmOAuthPasteEl.value.trim());
2435
+ }
2436
+ return Boolean(llmProviderEl.value.trim()) && Boolean(llmCredentialEl.value.trim());
2265
2437
  }
2266
2438
 
2267
2439
  /** All-or-nothing: 0 or 4 non-empty X fields; partial is invalid. */
@@ -2348,7 +2520,7 @@ function wizardHtml(defaults) {
2348
2520
  }
2349
2521
 
2350
2522
  function refreshModelOptions(preferredModel) {
2351
- const provider = llmProviderEl.value;
2523
+ const provider = effectiveLlmProvider();
2352
2524
  const providerEntry = (llmCatalog.providers || []).find((entry) => entry.id === provider);
2353
2525
  const modelItems = (providerEntry ? providerEntry.models : []).map((item) => ({ value: item.id, label: item.name + " (" + item.id + ")" }));
2354
2526
  if (modelItems.length === 0) {
@@ -2378,8 +2550,9 @@ function wizardHtml(defaults) {
2378
2550
  return;
2379
2551
  }
2380
2552
  setSelectOptions(llmProviderEl, providers, "${defaults.llmProvider}");
2553
+ applyLlmAuthModeUi();
2381
2554
  refreshModelOptions("${defaults.llmModel}");
2382
- const catalogMsg = "Select your provider, paste your API key, and start installation. After setup, run \`openclaw models list\` to explore your live catalog.";
2555
+ const catalogMsg = "Choose API key or Codex OAuth, then start installation. After setup, run \`openclaw models list\` for your live catalog.";
2383
2556
  setLlmCatalogReady(true, catalogMsg, false);
2384
2557
  } catch (err) {
2385
2558
  setLlmCatalogReady(false, "Failed to load LLM providers. Reload the page and try again.", true);
@@ -2433,10 +2606,14 @@ function wizardHtml(defaults) {
2433
2606
  manualEl.textContent = "";
2434
2607
  readyEl.textContent = "Starting installation...";
2435
2608
 
2609
+ const oauth = isOauthMode();
2436
2610
  const payload = {
2437
- llmProvider: llmProviderEl.value.trim(),
2611
+ llmAuthMode: oauth ? "oauth" : "api_key",
2612
+ llmProvider: oauth ? "openai-codex" : llmProviderEl.value.trim(),
2438
2613
  llmModel: llmModelEl.value.trim(),
2439
- llmCredential: llmCredentialEl.value.trim(),
2614
+ llmCredential: oauth ? "" : llmCredentialEl.value.trim(),
2615
+ llmOAuthPaste: llmOAuthPasteEl.value.trim(),
2616
+ llmOAuthSkipLogin: llmOAuthSkipLoginEl.checked,
2440
2617
  apiKey: document.getElementById("apiKey").value.trim(),
2441
2618
  telegramToken: document.getElementById("telegramToken").value.trim(),
2442
2619
  referralCode: document.getElementById("referralCode").value.trim(),
@@ -2445,10 +2622,22 @@ function wizardHtml(defaults) {
2445
2622
  xAccessTokenMain: xAccessTokenMainEl.value.trim(),
2446
2623
  xAccessTokenMainSecret: xAccessTokenMainSecretEl.value.trim(),
2447
2624
  };
2448
- if (!payload.llmProvider || !payload.llmCredential) {
2625
+ if (oauth && oauthWizardLoginDone) {
2626
+ payload.llmOAuthSkipLogin = true;
2627
+ payload.llmOAuthPaste = "";
2628
+ }
2629
+ if (oauth) {
2630
+ if (!payload.llmOAuthSkipLogin && !payload.llmOAuthPaste && !oauthWizardLoginDone) {
2631
+ stateEl.textContent = "blocked";
2632
+ readyEl.textContent = "";
2633
+ manualEl.textContent =
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.";
2635
+ return;
2636
+ }
2637
+ } else if (!payload.llmProvider || !payload.llmCredential) {
2449
2638
  stateEl.textContent = "blocked";
2450
2639
  readyEl.textContent = "";
2451
- manualEl.textContent = "LLM provider and credential are required before starting installation.";
2640
+ manualEl.textContent = "LLM provider and API key are required before starting installation.";
2452
2641
  return;
2453
2642
  }
2454
2643
  if (!payload.telegramToken) {
@@ -2690,7 +2879,130 @@ function wizardHtml(defaults) {
2690
2879
  llmModelEl.disabled = !llmModelManualEl.checked;
2691
2880
  updateStartButtonState();
2692
2881
  });
2882
+ llmAuthModeApiKey.addEventListener("change", onLlmAuthModeChange);
2883
+ llmAuthModeOauth.addEventListener("change", onLlmAuthModeChange);
2693
2884
  llmCredentialEl.addEventListener("input", updateStartButtonState);
2885
+ llmOAuthPasteEl.addEventListener("input", updateStartButtonState);
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
+
2694
3006
  telegramTokenEl.addEventListener("input", updateStartButtonState);
2695
3007
  xConsumerKeyEl.addEventListener("input", updateStartButtonState);
2696
3008
  xConsumerSecretEl.addEventListener("input", updateStartButtonState);
@@ -2732,6 +3044,31 @@ async function cmdInstall(args) {
2732
3044
  let running = false;
2733
3045
  let shuttingDown = false;
2734
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
+
2735
3072
  const server = createServer(async (req, res) => {
2736
3073
  const respondJson = (code, payload) => {
2737
3074
  res.statusCode = code;
@@ -2795,6 +3132,169 @@ async function cmdInstall(args) {
2795
3132
  return;
2796
3133
  }
2797
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
+
2798
3298
  if (req.method === "POST" && req.url === "/api/finish") {
2799
3299
  if (running || runtime.status !== "completed") {
2800
3300
  respondJson(409, { ok: false, error: "wizard_not_completed" });
@@ -2831,12 +3331,17 @@ async function cmdInstall(args) {
2831
3331
  }
2832
3332
 
2833
3333
  const body = await parseJsonBody(req).catch(() => ({}));
3334
+ const rawLlmAuth = body.llmAuthMode != null ? body.llmAuthMode : defaults.llmAuthMode;
2834
3335
  const wizardOpts = {
2835
3336
  mode: "light",
2836
3337
  lane: defaults.lane,
3338
+ llmAuthMode: rawLlmAuth === "oauth" ? "oauth" : "api_key",
2837
3339
  llmProvider: body.llmProvider || defaults.llmProvider,
2838
3340
  llmModel: body.llmModel || defaults.llmModel,
2839
3341
  llmCredential: body.llmCredential || defaults.llmCredential,
3342
+ llmOAuthPaste: typeof body.llmOAuthPaste === "string" ? body.llmOAuthPaste.trim() : defaults.llmOAuthPaste,
3343
+ llmOAuthSkipLogin:
3344
+ typeof body.llmOAuthSkipLogin === "boolean" ? body.llmOAuthSkipLogin : defaults.llmOAuthSkipLogin === true,
2840
3345
  apiKey: body.apiKey || defaults.apiKey,
2841
3346
  orchestratorUrl: defaults.orchestratorUrl,
2842
3347
  gatewayBaseUrl: defaults.gatewayBaseUrl,
@@ -2870,9 +3375,12 @@ async function cmdInstall(args) {
2870
3375
  {
2871
3376
  mode: "light",
2872
3377
  lane: defaults.lane,
3378
+ llmAuthMode: wizardOpts.llmAuthMode,
2873
3379
  llmProvider: body.llmProvider || defaults.llmProvider,
2874
3380
  llmModel: body.llmModel || defaults.llmModel,
2875
3381
  llmCredential: body.llmCredential || defaults.llmCredential,
3382
+ llmOAuthPaste: wizardOpts.llmOAuthPaste,
3383
+ llmOAuthSkipLogin: wizardOpts.llmOAuthSkipLogin,
2876
3384
  apiKey: body.apiKey || defaults.apiKey,
2877
3385
  orchestratorUrl: defaults.orchestratorUrl,
2878
3386
  gatewayBaseUrl: defaults.gatewayBaseUrl,
@@ -3240,6 +3748,15 @@ Config subcommands:
3240
3748
  config set <k> <v> Update a configuration value
3241
3749
  config reset Remove plugin configuration
3242
3750
 
3751
+ Install wizard (traderclaw install --wizard):
3752
+ --port Local port for the wizard (default 17890)
3753
+ --llm-provider e.g. openai, openai-codex, anthropic
3754
+ --llm-model e.g. openai/gpt-5.4 or openai-codex/gpt-5.4
3755
+ --llm-api-key, --llm-token API key for LLM (api_key mode)
3756
+ --llm-auth api_key|oauth OpenAI Platform key vs ChatGPT/Codex OAuth (openai-codex)
3757
+ --llm-oauth-paste Paste redirect URL or code for Codex OAuth (non-skip)
3758
+ --llm-oauth-skip-login Skip login when you already ran openclaw models auth login
3759
+
3243
3760
  Examples:
3244
3761
  traderclaw signup
3245
3762
  traderclaw setup
@@ -3249,6 +3766,7 @@ Examples:
3249
3766
  traderclaw precheck --allow-install
3250
3767
  traderclaw install --wizard
3251
3768
  traderclaw install --wizard --lane quick-local
3769
+ traderclaw install --wizard --llm-auth oauth --llm-provider openai-codex --llm-oauth-skip-login
3252
3770
  traderclaw repair-openclaw
3253
3771
  traderclaw gateway ensure-persistent
3254
3772
  traderclaw setup --signup --user-id my_agent_001 --referral-code ABCD1234
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "traderclaw-cli",
3
- "version": "1.0.73",
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.73"
20
+ "solana-traderclaw": "^1.0.75"
21
21
  },
22
22
  "keywords": [
23
23
  "traderclaw",