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.
- package/bin/installer-step-engine.mjs +178 -3
- package/bin/openclaw-trader.mjs +546 -28
- package/package.json +2 -2
|
@@ -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
|
-
|
|
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
|
|
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() {
|
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
|
}
|
|
@@ -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
|
|
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: [
|
|
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">
|
|
2073
|
-
<div
|
|
2074
|
-
<
|
|
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">
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
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 +
|
|
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">
|
|
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
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
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 =
|
|
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 = "
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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.
|
|
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",
|