solana-traderclaw 1.0.27 → 1.0.30
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/README.md +1 -1
- package/bin/installer-step-engine.mjs +91 -21
- package/bin/openclaw-trader.mjs +24 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -73,7 +73,7 @@ openclaw gateway restart
|
|
|
73
73
|
traderclaw install --wizard
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
This opens a localhost UI that runs prechecks, lane-aware setup, gateway validation, optional Telegram setup, and final verification. It also installs **`HEARTBEAT.md` into your OpenClaw agent workspace root** (default `~/.openclaw/workspace/HEARTBEAT.md`) from the packaged skill so heartbeats load the TraderClaw checklist without manual `cp`. `traderclaw setup` does the same.
|
|
76
|
+
This opens a localhost UI that runs prechecks, lane-aware setup, gateway validation, optional Telegram setup, and final verification. **X (Twitter) OAuth fields in the wizard are optional** — leave all four blank to run without X; you can add credentials later in `openclaw.json` or via env (`X_CONSUMER_KEY`, `X_CONSUMER_SECRET`, per-agent `X_ACCESS_TOKEN_*`). It also installs **`HEARTBEAT.md` into your OpenClaw agent workspace root** (default `~/.openclaw/workspace/HEARTBEAT.md`) from the packaged skill so heartbeats load the TraderClaw checklist without manual `cp`. `traderclaw setup` does the same.
|
|
77
77
|
|
|
78
78
|
### Optional: Run CLI prechecks directly
|
|
79
79
|
|
|
@@ -436,6 +436,7 @@ function seedPluginConfig(modeConfig, orchestratorUrl, configPath = CONFIG_FILE)
|
|
|
436
436
|
// Do not set plugins.allow here: OpenClaw validates allow[] against the plugin registry, and
|
|
437
437
|
// the id is not registered until after `openclaw plugins install`. Pre-seeding allow caused:
|
|
438
438
|
// "plugins.allow: plugin not found: <id>".
|
|
439
|
+
ensureAgentsDefaultsSchemaCompat(config);
|
|
439
440
|
|
|
440
441
|
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
441
442
|
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
@@ -576,6 +577,7 @@ function mergePluginsAllowlist(modeConfig, configPath = CONFIG_FILE) {
|
|
|
576
577
|
);
|
|
577
578
|
allowSet.add(modeConfig.pluginId);
|
|
578
579
|
config.plugins.allow = [...allowSet];
|
|
580
|
+
ensureAgentsDefaultsSchemaCompat(config);
|
|
579
581
|
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
580
582
|
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
581
583
|
}
|
|
@@ -985,6 +987,43 @@ async function verifyXCredentials(consumerKey, consumerSecret, accessToken, acce
|
|
|
985
987
|
return { ok: true, userId: data?.data?.id, username: data?.data?.username };
|
|
986
988
|
}
|
|
987
989
|
|
|
990
|
+
/** After OAuth verify, persist X user id + handle from GET /2/users/me into plugin config (no user typing). */
|
|
991
|
+
function persistXProfileIdentities(configPath, modeConfig, identities) {
|
|
992
|
+
if (!Array.isArray(identities) || identities.length === 0) return { written: 0 };
|
|
993
|
+
let config = {};
|
|
994
|
+
try {
|
|
995
|
+
config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
996
|
+
} catch {
|
|
997
|
+
return { written: 0 };
|
|
998
|
+
}
|
|
999
|
+
const entry = config?.plugins?.entries?.[modeConfig.pluginId];
|
|
1000
|
+
if (!entry?.config?.x?.profiles || typeof entry.config.x.profiles !== "object") return { written: 0 };
|
|
1001
|
+
|
|
1002
|
+
let profilesTouched = 0;
|
|
1003
|
+
for (const row of identities) {
|
|
1004
|
+
const agentId = row?.agentId;
|
|
1005
|
+
const userId = row?.userId;
|
|
1006
|
+
const username = row?.username;
|
|
1007
|
+
if (typeof agentId !== "string" || !agentId.length) continue;
|
|
1008
|
+
const p = entry.config.x.profiles[agentId];
|
|
1009
|
+
if (!p || typeof p !== "object") continue;
|
|
1010
|
+
let touched = false;
|
|
1011
|
+
if (userId != null && String(userId).length > 0) {
|
|
1012
|
+
p.userId = String(userId);
|
|
1013
|
+
touched = true;
|
|
1014
|
+
}
|
|
1015
|
+
if (username != null && String(username).length > 0) {
|
|
1016
|
+
p.username = String(username);
|
|
1017
|
+
touched = true;
|
|
1018
|
+
}
|
|
1019
|
+
if (touched) profilesTouched++;
|
|
1020
|
+
}
|
|
1021
|
+
if (profilesTouched === 0) return { written: 0 };
|
|
1022
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
1023
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
1024
|
+
return { written: profilesTouched };
|
|
1025
|
+
}
|
|
1026
|
+
|
|
988
1027
|
function listProviderModels(provider) {
|
|
989
1028
|
const cmd = `openclaw models list --all --provider ${shellQuote(provider)} --json`;
|
|
990
1029
|
const raw = getCommandOutput(cmd);
|
|
@@ -1045,6 +1084,40 @@ function resolveLlmModelSelection(provider, requestedModel) {
|
|
|
1045
1084
|
return { model: fallbackModelForProvider(provider), source: "fallback_guess", availableModels, warnings };
|
|
1046
1085
|
}
|
|
1047
1086
|
|
|
1087
|
+
/**
|
|
1088
|
+
* OpenClaw 2026+ validates `agents.defaults` with Zod/Ajv: if `defaults` exists it must include
|
|
1089
|
+
* `heartbeat` and `model` objects. `openclaw plugins install/enable` can merge config and drop
|
|
1090
|
+
* these keys, which surfaces as obscure stack errors (e.g. "ajv implementation error") on the next CLI call.
|
|
1091
|
+
*/
|
|
1092
|
+
function ensureAgentsDefaultsSchemaCompat(config) {
|
|
1093
|
+
if (!config || typeof config !== "object") return;
|
|
1094
|
+
if (!config.agents || typeof config.agents !== "object") return;
|
|
1095
|
+
if (!config.agents.defaults || typeof config.agents.defaults !== "object") return;
|
|
1096
|
+
if (!config.agents.defaults.heartbeat || typeof config.agents.defaults.heartbeat !== "object") {
|
|
1097
|
+
config.agents.defaults.heartbeat = {};
|
|
1098
|
+
}
|
|
1099
|
+
if (!config.agents.defaults.model || typeof config.agents.defaults.model !== "object") {
|
|
1100
|
+
config.agents.defaults.model = {};
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/** Re-read config from disk and re-apply defaults shape before gateway/plugin commands that validate the file. */
|
|
1105
|
+
function normalizeOpenClawConfigFileShape(configPath = CONFIG_FILE) {
|
|
1106
|
+
let config = {};
|
|
1107
|
+
try {
|
|
1108
|
+
config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
1109
|
+
} catch {
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
ensureAgentsDefaultsSchemaCompat(config);
|
|
1113
|
+
try {
|
|
1114
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
1115
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
1116
|
+
} catch {
|
|
1117
|
+
// best effort
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1048
1121
|
function configureOpenClawLlmProvider({ provider, model, credential }, configPath = CONFIG_FILE) {
|
|
1049
1122
|
if (!provider || !credential) {
|
|
1050
1123
|
throw new Error("LLM provider and credential are required.");
|
|
@@ -1086,15 +1159,7 @@ function configureOpenClawLlmProvider({ provider, model, credential }, configPat
|
|
|
1086
1159
|
|
|
1087
1160
|
if (!config.agents) config.agents = {};
|
|
1088
1161
|
if (!config.agents.defaults) config.agents.defaults = {};
|
|
1089
|
-
|
|
1090
|
-
// (see OpenClaw AgentDefaultsSchema). Omitting it makes openclaw plugins install fail at
|
|
1091
|
-
// writeConfigFile → validateConfigObjectRaw with a stack-only error in the UI.
|
|
1092
|
-
if (!config.agents.defaults.heartbeat || typeof config.agents.defaults.heartbeat !== "object") {
|
|
1093
|
-
config.agents.defaults.heartbeat = {};
|
|
1094
|
-
}
|
|
1095
|
-
if (!config.agents.defaults.model || typeof config.agents.defaults.model !== "object") {
|
|
1096
|
-
config.agents.defaults.model = {};
|
|
1097
|
-
}
|
|
1162
|
+
ensureAgentsDefaultsSchemaCompat(config);
|
|
1098
1163
|
config.agents.defaults.model.primary = model;
|
|
1099
1164
|
|
|
1100
1165
|
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
@@ -1550,6 +1615,7 @@ export class InstallerStepEngine {
|
|
|
1550
1615
|
if (!this.options.skipGatewayBootstrap) {
|
|
1551
1616
|
await this.runStep("gateway_bootstrap", "Starting OpenClaw gateway and Funnel", async () => {
|
|
1552
1617
|
try {
|
|
1618
|
+
normalizeOpenClawConfigFileShape(CONFIG_FILE);
|
|
1553
1619
|
await this.runWithPrivilegeGuidance("gateway_bootstrap", "openclaw", ["gateway", "install"]);
|
|
1554
1620
|
await this.runWithPrivilegeGuidance("gateway_bootstrap", "openclaw", ["gateway", "restart"]);
|
|
1555
1621
|
return this.runFunnel();
|
|
@@ -1672,6 +1738,7 @@ export class InstallerStepEngine {
|
|
|
1672
1738
|
}
|
|
1673
1739
|
const { consumerKey, consumerSecret } = getConsumerKeysFromWizard(this.options);
|
|
1674
1740
|
const verified = [];
|
|
1741
|
+
const identitiesToPersist = [];
|
|
1675
1742
|
for (const agentId of result.agentIds) {
|
|
1676
1743
|
const { at, ats } = getAccessPairForAgent(this.options, agentId);
|
|
1677
1744
|
if (at && ats) {
|
|
@@ -1680,6 +1747,7 @@ export class InstallerStepEngine {
|
|
|
1680
1747
|
if (check.ok) {
|
|
1681
1748
|
this.emitLog("x_credentials", "info", `Verified X profile '${agentId}': @${check.username} (${check.userId})`);
|
|
1682
1749
|
verified.push({ agentId, username: check.username, userId: check.userId });
|
|
1750
|
+
identitiesToPersist.push({ agentId, userId: check.userId, username: check.username });
|
|
1683
1751
|
} else {
|
|
1684
1752
|
this.emitLog("x_credentials", "warn", `X credential verification failed for '${agentId}': HTTP ${check.status}`);
|
|
1685
1753
|
}
|
|
@@ -1688,6 +1756,12 @@ export class InstallerStepEngine {
|
|
|
1688
1756
|
}
|
|
1689
1757
|
}
|
|
1690
1758
|
}
|
|
1759
|
+
if (identitiesToPersist.length > 0) {
|
|
1760
|
+
const persisted = persistXProfileIdentities(CONFIG_FILE, this.modeConfig, identitiesToPersist);
|
|
1761
|
+
if (persisted.written > 0) {
|
|
1762
|
+
this.emitLog("x_credentials", "info", `Saved X user id and username to openclaw.json for ${persisted.written} profile(s) (from API, not manual entry).`);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1691
1765
|
return { ...result, verified };
|
|
1692
1766
|
});
|
|
1693
1767
|
|
|
@@ -1713,18 +1787,14 @@ export class InstallerStepEngine {
|
|
|
1713
1787
|
export function assertWizardXCredentials(modeConfig, options = {}) {
|
|
1714
1788
|
const t = (s) => (typeof s === "string" ? s.trim() : "");
|
|
1715
1789
|
const o = options || {};
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
for (const k of need) {
|
|
1725
|
-
if (!t(o[k])) return `Missing required X/Twitter field: ${k}`;
|
|
1726
|
-
}
|
|
1727
|
-
return null;
|
|
1790
|
+
const need =
|
|
1791
|
+
modeConfig.pluginId === "solana-trader-v2"
|
|
1792
|
+
? ["xConsumerKey", "xConsumerSecret", "xAccessTokenCto", "xAccessTokenCtoSecret", "xAccessTokenIntern", "xAccessTokenInternSecret"]
|
|
1793
|
+
: ["xConsumerKey", "xConsumerSecret", "xAccessTokenMain", "xAccessTokenMainSecret"];
|
|
1794
|
+
const filled = need.filter((k) => t(o[k])).length;
|
|
1795
|
+
if (filled === 0) return null;
|
|
1796
|
+
if (filled === need.length) return null;
|
|
1797
|
+
return `X/Twitter credentials are optional: leave all ${need.length} fields blank, or fill every field (OAuth app key/secret plus user access token and secret for each profile).`;
|
|
1728
1798
|
}
|
|
1729
1799
|
|
|
1730
1800
|
export function createInstallerStepEngine(modeConfig, options = {}, hooks = {}) {
|
package/bin/openclaw-trader.mjs
CHANGED
|
@@ -1791,29 +1791,29 @@ function wizardHtml(defaults) {
|
|
|
1791
1791
|
</div>
|
|
1792
1792
|
</div>
|
|
1793
1793
|
<div class="card" id="xCard">
|
|
1794
|
-
<h3>
|
|
1795
|
-
<p class="muted">
|
|
1794
|
+
<h3>Optional: X (Twitter) OAuth 1.0a</h3>
|
|
1795
|
+
<p class="muted">Skip this section to use trading and Telegram without X. When set, app keys plus user access token enable the <code>main</code> agent profile (journal & engagement tools).</p>
|
|
1796
1796
|
<div class="grid">
|
|
1797
1797
|
<div>
|
|
1798
|
-
<label>X consumer key (
|
|
1798
|
+
<label>X consumer key (optional)</label>
|
|
1799
1799
|
<input id="xConsumerKey" type="password" autocomplete="off" placeholder="From your X Developer App" />
|
|
1800
1800
|
</div>
|
|
1801
1801
|
<div>
|
|
1802
|
-
<label>X consumer secret (
|
|
1802
|
+
<label>X consumer secret (optional)</label>
|
|
1803
1803
|
<input id="xConsumerSecret" type="password" autocomplete="off" />
|
|
1804
1804
|
</div>
|
|
1805
1805
|
</div>
|
|
1806
1806
|
<div class="grid" style="margin-top:12px;">
|
|
1807
1807
|
<div>
|
|
1808
|
-
<label>X access token — main profile (
|
|
1808
|
+
<label>X access token — main profile (optional)</label>
|
|
1809
1809
|
<input id="xAccessTokenMain" type="password" autocomplete="off" placeholder="User access token for the posting account" />
|
|
1810
1810
|
</div>
|
|
1811
1811
|
<div>
|
|
1812
|
-
<label>X access token secret — main profile (
|
|
1812
|
+
<label>X access token secret — main profile (optional)</label>
|
|
1813
1813
|
<input id="xAccessTokenMainSecret" type="password" autocomplete="off" />
|
|
1814
1814
|
</div>
|
|
1815
1815
|
</div>
|
|
1816
|
-
<p class="muted">
|
|
1816
|
+
<p class="muted">If you use X: create an app at <a href="https://developer.x.com" target="_blank" rel="noopener noreferrer">developer.x.com</a> with OAuth 1.0a Read and Write, and fill all four fields above (or leave all blank). Values are written to <code>openclaw.json</code> under the plugin <code>x</code> block.</p>
|
|
1817
1817
|
</div>
|
|
1818
1818
|
<div class="card" id="startCard">
|
|
1819
1819
|
<div class="grid">
|
|
@@ -1946,13 +1946,21 @@ function wizardHtml(defaults) {
|
|
|
1946
1946
|
&& Boolean(llmProviderEl.value.trim())
|
|
1947
1947
|
&& Boolean(llmCredentialEl.value.trim())
|
|
1948
1948
|
&& Boolean(telegramTokenEl.value.trim())
|
|
1949
|
-
&& Boolean(xConsumerKeyEl.value.trim())
|
|
1950
|
-
&& Boolean(xConsumerSecretEl.value.trim())
|
|
1951
|
-
&& Boolean(xAccessTokenMainEl.value.trim())
|
|
1952
|
-
&& Boolean(xAccessTokenMainSecretEl.value.trim())
|
|
1953
1949
|
);
|
|
1954
1950
|
}
|
|
1955
1951
|
|
|
1952
|
+
/** All-or-nothing: 0 or 4 non-empty X fields; partial is invalid. */
|
|
1953
|
+
function xWizardFieldsStatus() {
|
|
1954
|
+
const fields = [
|
|
1955
|
+
xConsumerKeyEl.value.trim(),
|
|
1956
|
+
xConsumerSecretEl.value.trim(),
|
|
1957
|
+
xAccessTokenMainEl.value.trim(),
|
|
1958
|
+
xAccessTokenMainSecretEl.value.trim(),
|
|
1959
|
+
];
|
|
1960
|
+
const filled = fields.filter(Boolean).length;
|
|
1961
|
+
return { filled, total: 4, ok: filled === 0 || filled === 4 };
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1956
1964
|
function updateStartButtonState() {
|
|
1957
1965
|
if (installLocked) {
|
|
1958
1966
|
startBtn.disabled = true;
|
|
@@ -1963,7 +1971,7 @@ function wizardHtml(defaults) {
|
|
|
1963
1971
|
return;
|
|
1964
1972
|
}
|
|
1965
1973
|
startBtn.removeAttribute("aria-busy");
|
|
1966
|
-
startBtn.disabled = llmCatalogLoading || !hasRequiredInputs();
|
|
1974
|
+
startBtn.disabled = llmCatalogLoading || !hasRequiredInputs() || !xWizardFieldsStatus().ok;
|
|
1967
1975
|
if (!llmCatalogLoading) {
|
|
1968
1976
|
startBtn.textContent = "Start Installation";
|
|
1969
1977
|
}
|
|
@@ -2132,10 +2140,12 @@ function wizardHtml(defaults) {
|
|
|
2132
2140
|
manualEl.textContent = "Telegram bot token is required before starting installation.";
|
|
2133
2141
|
return;
|
|
2134
2142
|
}
|
|
2135
|
-
|
|
2143
|
+
const xStatus = xWizardFieldsStatus();
|
|
2144
|
+
if (!xStatus.ok) {
|
|
2136
2145
|
stateEl.textContent = "blocked";
|
|
2137
2146
|
readyEl.textContent = "";
|
|
2138
|
-
manualEl.textContent =
|
|
2147
|
+
manualEl.textContent =
|
|
2148
|
+
"X (Twitter) credentials are optional: leave all four fields blank, or fill consumer key, consumer secret, access token, and access token secret.";
|
|
2139
2149
|
return;
|
|
2140
2150
|
}
|
|
2141
2151
|
|
package/package.json
CHANGED