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 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
- // OpenClaw 2026+ Zod schema requires agents.defaults.heartbeat whenever defaults exists
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
- if (modeConfig.pluginId === "solana-trader-v2") {
1717
- const need = ["xConsumerKey", "xConsumerSecret", "xAccessTokenCto", "xAccessTokenCtoSecret", "xAccessTokenIntern", "xAccessTokenInternSecret"];
1718
- for (const k of need) {
1719
- if (!t(o[k])) return `Missing required X/Twitter field: ${k}`;
1720
- }
1721
- return null;
1722
- }
1723
- const need = ["xConsumerKey", "xConsumerSecret", "xAccessTokenMain", "xAccessTokenMainSecret"];
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 = {}) {
@@ -1791,29 +1791,29 @@ function wizardHtml(defaults) {
1791
1791
  </div>
1792
1792
  </div>
1793
1793
  <div class="card" id="xCard">
1794
- <h3>Required: X (Twitter) OAuth 1.0a</h3>
1795
- <p class="muted">App keys plus user access token for the <code>main</code> agent profile (journal &amp; engagement tools).</p>
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 &amp; engagement tools).</p>
1796
1796
  <div class="grid">
1797
1797
  <div>
1798
- <label>X consumer key (required)</label>
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 (required)</label>
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 (required)</label>
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 (required)</label>
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">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. Values are written to <code>openclaw.json</code> under the plugin <code>x</code> block.</p>
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
- if (!payload.xConsumerKey || !payload.xConsumerSecret || !payload.xAccessTokenMain || !payload.xAccessTokenMainSecret) {
2143
+ const xStatus = xWizardFieldsStatus();
2144
+ if (!xStatus.ok) {
2136
2145
  stateEl.textContent = "blocked";
2137
2146
  readyEl.textContent = "";
2138
- manualEl.textContent = "X (Twitter) consumer key, consumer secret, access token, and access token secret are required before starting installation.";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solana-traderclaw",
3
- "version": "1.0.27",
3
+ "version": "1.0.30",
4
4
  "description": "TraderClaw V1 — autonomous Solana memecoin trading for OpenClaw (team edition: X/Twitter journal and engagement tools)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",