traderclaw-cli 1.0.95-beta.0 → 1.0.96

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.
@@ -2,7 +2,6 @@ import { execFileSync, execSync, spawn } from "child_process";
2
2
  import { randomBytes } from "crypto";
3
3
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, statSync, writeFileSync } from "fs";
4
4
  import { homedir, tmpdir } from "os";
5
- import * as os from "os";
6
5
  import { dirname, join } from "path";
7
6
  import { resolvePluginPackageRoot } from "./resolve-plugin-root.mjs";
8
7
  import { choosePreferredProviderModel } from "./llm-model-preference.mjs";
@@ -213,11 +212,7 @@ const NO_COLOR_ENV = { ...process.env, NO_COLOR: "1", FORCE_COLOR: "0" };
213
212
  * The current OpenClaw approach (docs.openclaw.ai/channels/telegram):
214
213
  * channels.telegram.botToken = "<token>" → token source
215
214
  * channels.telegram.enabled = true → enable the channel
216
- * channels.telegram.dmPolicy = "open" single-user wizard install: deliver DMs without pairing
217
- *
218
- * Note: "pairing" is safer for multi-tenant deployments but breaks the wizard onboarding flow
219
- * (heartbeat fires before the user has paired, so the very first DM is dropped). Single-user
220
- * wizard installs almost always want "open"; tighten later if needed.
215
+ * channels.telegram.dmPolicy = "pairing" safe default (user approves first DM)
221
216
  */
222
217
  function writeTelegramChannelConfig(botToken, configPath = CONFIG_FILE) {
223
218
  let config = {};
@@ -232,7 +227,7 @@ function writeTelegramChannelConfig(botToken, configPath = CONFIG_FILE) {
232
227
  config.channels.telegram.botToken = botToken;
233
228
  // Only set dmPolicy if not already configured (preserve existing policy on re-installs).
234
229
  if (!config.channels.telegram.dmPolicy) {
235
- config.channels.telegram.dmPolicy = "open";
230
+ config.channels.telegram.dmPolicy = "pairing";
236
231
  }
237
232
  ensureAgentsDefaultsSchemaCompat(config);
238
233
  mkdirSync(CONFIG_DIR, { recursive: true });
@@ -535,6 +530,42 @@ async function installOpenClawPlatform() {
535
530
  };
536
531
  }
537
532
 
533
+ /**
534
+ * Check whether the OpenClaw CLI has any devices stuck in a pending-approval or
535
+ * repair state. This can happen when the gateway version >= 1.0.93-beta.0 starts
536
+ * treating every CLI invocation as a "device" that must be explicitly approved
537
+ * before it gets operator-write scope. Without that scope all trading RPCs fail
538
+ * silently (read-only).
539
+ *
540
+ * Returns:
541
+ * { ran: false } – openclaw not on PATH or devices subcommand not supported
542
+ * { ran: true, pendingIds: string[], – list ran OK; ids needing approval
543
+ * repairDetected: boolean, – current device is in repair/read-only state
544
+ * envTokenSet: boolean } – OPENCLAW_GATEWAY_TOKEN env var already present (fallback)
545
+ */
546
+ function checkOpenClawDeviceApproval() {
547
+ if (!commandExists("openclaw")) return { ran: false };
548
+ const raw = getCommandOutput("openclaw devices list");
549
+ if (!raw) return { ran: false };
550
+
551
+ const lower = raw.toLowerCase();
552
+ const envTokenSet = !!process.env.OPENCLAW_GATEWAY_TOKEN;
553
+
554
+ // Detect devices that are waiting for approval ("pending" requestId lines).
555
+ const pendingIds = [];
556
+ for (const line of raw.split("\n")) {
557
+ // Lines typically look like: d4fcdbe8-5176-422b-... pending
558
+ const m = line.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i);
559
+ if (m && (line.toLowerCase().includes("pending") || line.toLowerCase().includes("repair"))) {
560
+ pendingIds.push(m[1]);
561
+ }
562
+ }
563
+
564
+ const repairDetected = lower.includes("repair");
565
+
566
+ return { ran: true, pendingIds, repairDetected, envTokenSet, raw };
567
+ }
568
+
538
569
  function isNpmGlobalBinConflict(err, cliName) {
539
570
  const text = `${err?.message || ""}\n${err?.stderr || ""}\n${err?.stdout || ""}`.toLowerCase();
540
571
  return (
@@ -867,46 +898,13 @@ function mergePluginsAllowlist(modeConfig, configPath = CONFIG_FILE) {
867
898
  writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
868
899
  }
869
900
 
870
- /**
871
- * Resolve per-provider model strings used by managed cron jobs.
872
- * Returns `{ heavy, light }`. `heavy` is the wizard-chosen model. `light` is a cheaper variant
873
- * when the provider has one (Anthropic → Haiku); otherwise `light` falls back to `heavy`.
874
- *
875
- * This replaces the previous hardcoded `anthropic/claude-sonnet-4-20250514` and
876
- * `anthropic/claude-haiku-4-5` cron models, which silently failed for Codex / OpenAI / etc.
877
- * installs because the user never authenticated with Anthropic.
878
- */
879
- function resolveCronModels(provider, model) {
880
- const heavy = (typeof model === "string" && model.startsWith(`${provider}/`)) ? model : null;
881
- if (provider === "anthropic") {
882
- return {
883
- heavy: heavy || "anthropic/claude-sonnet-4-6",
884
- light: "anthropic/claude-haiku-4-5",
885
- };
886
- }
887
- if (provider === "openai-codex") {
888
- const m = heavy || "openai-codex/gpt-5.4";
889
- return { heavy: m, light: m };
890
- }
891
- if (provider === "openai") {
892
- const m = heavy || "openai/gpt-5.4";
893
- return { heavy: m, light: m };
894
- }
895
- // Generic fallback: same model for both tiers (configured provider determines the model string).
896
- const fallback = heavy || `${provider}/default`;
897
- return { heavy: fallback, light: fallback };
898
- }
899
-
900
901
  /**
901
902
  * Managed cron jobs with prescriptive tool chains (VPS report 2026-03-24).
902
903
  * Schedules are staggered (minutes :00 / :15 / :30 / :45) where possible to avoid pile-ups.
903
904
  * @param {string} agentId
904
- * @param {{ heavy: string, light: string }} models
905
905
  * @returns {Array<{ id: string, schedule: string, agentId: string, message: string, enabled: boolean }>}
906
906
  */
907
- function traderCronPrescriptiveJobs(agentId, models = { heavy: "anthropic/claude-sonnet-4-6", light: "anthropic/claude-haiku-4-5" }) {
908
- const HEAVY = models.heavy;
909
- const LIGHT = models.light;
907
+ function traderCronPrescriptiveJobs(agentId) {
910
908
  return [
911
909
  {
912
910
  id: "alpha-scan",
@@ -914,7 +912,7 @@ function traderCronPrescriptiveJobs(agentId, models = { heavy: "anthropic/claude
914
912
  agentId,
915
913
  message:
916
914
  "CRON_JOB: alpha_scan\n\nScan new launches, filter, score, log alpha. Tools: solana_scan_launches → filter (vol>30K, mcap>10K, liq>5K) → solana_token_snapshot for survivors → quality filter (top10 <50%, deployer <3 abandoned, has social) → score 0-100 → solana_alpha_log for 65+. Summarize results.",
917
- model: HEAVY,
915
+ model: "anthropic/claude-sonnet-4-20250514",
918
916
  thinking: false,
919
917
  lightContext: true,
920
918
  delivery: { mode: "announce", channel: "last", bestEffort: true },
@@ -926,7 +924,7 @@ function traderCronPrescriptiveJobs(agentId, models = { heavy: "anthropic/claude
926
924
  agentId,
927
925
  message:
928
926
  "CRON_JOB: portfolio_health\n\nCombined dead-money + whale + risk audit. solana_capital_status + solana_positions → solana_token_snapshot per position → dead money exit (loss>40% or 90min+down+low vol) → whale flags (>5% supply moves) → risk checks (concentration/drawdown/exposure) → sell if CRITICAL → solana_memory_write tag 'portfolio_health'.",
929
- model: HEAVY,
927
+ model: "anthropic/claude-sonnet-4-20250514",
930
928
  thinking: false,
931
929
  lightContext: true,
932
930
  delivery: { mode: "announce", channel: "last", bestEffort: true },
@@ -938,7 +936,7 @@ function traderCronPrescriptiveJobs(agentId, models = { heavy: "anthropic/claude
938
936
  agentId,
939
937
  message:
940
938
  "CRON_JOB: trust_refresh\n\nCombined source + deployer trust. solana_source_trust_refresh + solana_deployer_trust_refresh → solana_alpha_sources + solana_trades for win rates → solana_source_trust_get + solana_deployer_trust_get, flag <30 → solana_memory_write tag 'trust_refresh'.",
941
- model: LIGHT,
939
+ model: "anthropic/claude-haiku-4-5",
942
940
  thinking: false,
943
941
  lightContext: true,
944
942
  delivery: { mode: "none" },
@@ -950,7 +948,7 @@ function traderCronPrescriptiveJobs(agentId, models = { heavy: "anthropic/claude
950
948
  agentId,
951
949
  message:
952
950
  "CRON_JOB: meta_rotation_analysis\n\nx_search_tweets trending topics → solana_scan_launches → categorize by narrative cluster → per-cluster metrics → compare vs solana_memory_search tag 'meta_rotation' → declare hot/fading clusters → solana_memory_write tag 'meta_rotation'.",
953
- model: HEAVY,
951
+ model: "anthropic/claude-sonnet-4-20250514",
954
952
  thinking: false,
955
953
  lightContext: true,
956
954
  delivery: { mode: "announce", channel: "last", bestEffort: true },
@@ -962,7 +960,7 @@ function traderCronPrescriptiveJobs(agentId, models = { heavy: "anthropic/claude
962
960
  agentId,
963
961
  message:
964
962
  "CRON_JOB: strategy_evolution\n\nDaily strategy review. solana_journal_summary — if <10 closed trades since last evolution, log 'insufficient data' and stop. Otherwise: solana_trades to bucket by confidence tier → solana_strategy_state for current weights → analyze tier performance → solana_strategy_update with conservative adjustments (max 10% per weight per cycle) → solana_memory_write tag 'strategy_evolution'.",
965
- model: HEAVY,
963
+ model: "anthropic/claude-sonnet-4-20250514",
966
964
  thinking: true,
967
965
  lightContext: false,
968
966
  delivery: { mode: "announce", channel: "last", bestEffort: true },
@@ -974,7 +972,7 @@ function traderCronPrescriptiveJobs(agentId, models = { heavy: "anthropic/claude
974
972
  agentId,
975
973
  message:
976
974
  "CRON_JOB: subscription_cleanup\n\nsolana_positions for open CAs → solana_bitquery_subscriptions for active subs (if AUTH_SCOPE_MISSING, log and stop) → match subs to positions → solana_bitquery_unsubscribe orphaned subs → solana_memory_write tag 'subscription_cleanup'. Summarize before/after counts.",
977
- model: LIGHT,
975
+ model: "anthropic/claude-haiku-4-5",
978
976
  thinking: false,
979
977
  lightContext: true,
980
978
  delivery: { mode: "announce", channel: "last", bestEffort: true },
@@ -986,7 +984,7 @@ function traderCronPrescriptiveJobs(agentId, models = { heavy: "anthropic/claude
986
984
  agentId,
987
985
  message:
988
986
  "CRON_JOB: daily_performance_report\n\nCompile 24h report. solana_journal_summary + solana_capital_status + solana_positions + solana_trades + solana_strategy_state → sections: Portfolio Summary, Trading Activity (count/win rate/PnL), Best/Worst Trades, Strategy State, Risk Metrics, Recommendations → solana_memory_write tag 'daily_report'. Deliver full report.",
989
- model: HEAVY,
987
+ model: "anthropic/claude-sonnet-4-20250514",
990
988
  thinking: false,
991
989
  lightContext: false,
992
990
  delivery: { mode: "announce", channel: "telegram" },
@@ -998,7 +996,7 @@ function traderCronPrescriptiveJobs(agentId, models = { heavy: "anthropic/claude
998
996
  agentId,
999
997
  message:
1000
998
  "CRON_JOB: intelligence_lab_eval\n\nsolana_candidate_get — if <20 labeled candidates, log 'insufficient data' and exit. Otherwise: solana_evaluation_report → solana_model_registry for challengers → solana_replay_eval if challenger exists → solana_model_promote if challenger beats champion by >5% F1 → solana_memory_write tag 'intelligence_lab'.",
1001
- model: HEAVY,
999
+ model: "anthropic/claude-sonnet-4-20250514",
1002
1000
  thinking: true,
1003
1001
  lightContext: false,
1004
1002
  delivery: { mode: "none" },
@@ -1010,7 +1008,7 @@ function traderCronPrescriptiveJobs(agentId, models = { heavy: "anthropic/claude
1010
1008
  agentId,
1011
1009
  message:
1012
1010
  "CRON_JOB: memory_trim\n\nsolana_memory_trim dryRun:true first → review → solana_memory_trim retentionDays:2 → solana_memory_write tag 'memory_trim' with summary.",
1013
- model: LIGHT,
1011
+ model: "anthropic/claude-haiku-4-5",
1014
1012
  thinking: false,
1015
1013
  lightContext: true,
1016
1014
  delivery: { mode: "none" },
@@ -1022,7 +1020,7 @@ function traderCronPrescriptiveJobs(agentId, models = { heavy: "anthropic/claude
1022
1020
  agentId,
1023
1021
  message:
1024
1022
  "Balance watchdog. 1) solana_capital_status 2) solana_positions 3) solana_context_snapshot_read 4) Compare real vs believed. If mismatch: solana_context_snapshot_write with corrected state, summarize changes. If match: reply WATCHDOG_OK.",
1025
- model: LIGHT,
1023
+ model: "anthropic/claude-haiku-4-5",
1026
1024
  thinking: false,
1027
1025
  lightContext: true,
1028
1026
  delivery: { mode: "announce", channel: "telegram" },
@@ -1031,7 +1029,7 @@ function traderCronPrescriptiveJobs(agentId, models = { heavy: "anthropic/claude
1031
1029
  ];
1032
1030
  }
1033
1031
 
1034
- function configureGatewayScheduling(modeConfig, configPath = CONFIG_FILE, options = {}) {
1032
+ function configureGatewayScheduling(modeConfig, configPath = CONFIG_FILE) {
1035
1033
  let config = {};
1036
1034
  try {
1037
1035
  config = JSON.parse(readFileSync(configPath, "utf-8"));
@@ -1049,12 +1047,9 @@ function configureGatewayScheduling(modeConfig, configPath = CONFIG_FILE, option
1049
1047
  /** Default periodic wake interval for TraderClaw installs (was 5m; stretched to reduce load). */
1050
1048
  const defaultHeartbeatEvery = "30m";
1051
1049
 
1052
- // target: "last" routes to the last channel that DM'd the agent (works as soon as the user
1053
- // sends any message in Telegram). "telegram" requires per-channel default routing setup
1054
- // and silently drops on first install — see VPS post-mortem.
1055
1050
  const defaultHeartbeat = {
1056
1051
  every: defaultHeartbeatEvery,
1057
- target: "last",
1052
+ target: "telegram",
1058
1053
  isolatedSession: true,
1059
1054
  lightContext: true,
1060
1055
  prompt: heartbeatPrompt,
@@ -1084,18 +1079,7 @@ function configureGatewayScheduling(modeConfig, configPath = CONFIG_FILE, option
1084
1079
  if (existingIds.has(agent.id)) {
1085
1080
  const existing = config.agents.list.find(a => a.id === agent.id);
1086
1081
  if (agent.heartbeat) {
1087
- // Idempotent reinstall: never clobber an existing user-customized heartbeat block.
1088
- // Only seed defaults that are missing — preserve any per-key overrides the operator
1089
- // wrote between installs (cadence, tone, target=@username, etc.).
1090
- if (!existing.heartbeat || typeof existing.heartbeat !== "object") {
1091
- existing.heartbeat = { ...agent.heartbeat };
1092
- } else {
1093
- for (const [k, v] of Object.entries(agent.heartbeat)) {
1094
- if (existing.heartbeat[k] === undefined || existing.heartbeat[k] === null || existing.heartbeat[k] === "") {
1095
- existing.heartbeat[k] = v;
1096
- }
1097
- }
1098
- }
1082
+ existing.heartbeat = agent.heartbeat;
1099
1083
  }
1100
1084
  if (agent.default) {
1101
1085
  existing.default = true;
@@ -1118,11 +1102,7 @@ function configureGatewayScheduling(modeConfig, configPath = CONFIG_FILE, option
1118
1102
  const mainAgent = isV2 ? "cto" : "main";
1119
1103
 
1120
1104
  /** Six prescriptive managed jobs (VPS report); v2 assigns the same set to the CTO agent. */
1121
- const cronModels = resolveCronModels(
1122
- String(options?.llmProvider || "anthropic").trim(),
1123
- String(options?.llmModel || "").trim(),
1124
- );
1125
- const targetJobs = traderCronPrescriptiveJobs(mainAgent, cronModels);
1105
+ const targetJobs = traderCronPrescriptiveJobs(mainAgent);
1126
1106
 
1127
1107
  let removedLegacyCronJobs = false;
1128
1108
  if (config.cron && Object.prototype.hasOwnProperty.call(config.cron, "jobs")) {
@@ -1191,39 +1171,6 @@ function configureGatewayScheduling(modeConfig, configPath = CONFIG_FILE, option
1191
1171
  }
1192
1172
  config.agents.defaults.heartbeat = { ...defaultHeartbeat };
1193
1173
 
1194
- if (!config.agents.defaults.memorySearch || typeof config.agents.defaults.memorySearch !== "object") {
1195
- config.agents.defaults.memorySearch = {
1196
- provider: "openai",
1197
- model: "text-embedding-3-small",
1198
- query: {
1199
- hybrid: true,
1200
- mmr: { enabled: true, lambda: 0.5 },
1201
- temporalDecay: { enabled: true, halfLifeDays: 14 },
1202
- },
1203
- };
1204
- }
1205
-
1206
- if (!config.agents.defaults.contextPruning || typeof config.agents.defaults.contextPruning !== "object") {
1207
- config.agents.defaults.contextPruning = { cacheTtl: "1h" };
1208
- }
1209
-
1210
- if (!config.env || typeof config.env !== "object") config.env = {};
1211
- if (process.env.OPENAI_API_KEY && !config.env.OPENAI_API_KEY) {
1212
- // Literal env reference (not the value). The gateway expands ${OPENAI_API_KEY}
1213
- // at runtime from its process environment. Keeps the secret out of the
1214
- // config file on disk.
1215
- config.env.OPENAI_API_KEY = "${OPENAI_API_KEY}";
1216
- } else if (!process.env.OPENAI_API_KEY && !config.env.OPENAI_API_KEY && typeof console !== "undefined") {
1217
- console.warn(
1218
- "[traderclaw] OPENAI_API_KEY not detected in the installer shell. " +
1219
- "Memory embeddings require it for vector search (text-embedding-3-small, ~$0.02/M tokens).\n" +
1220
- "Set it and re-run the installer, or add the env ref manually:\n" +
1221
- " 1) export OPENAI_API_KEY=sk-...\n" +
1222
- " 2) edit ~/.openclaw/openclaw.json and set env.OPENAI_API_KEY to \"${OPENAI_API_KEY}\"\n" +
1223
- " 3) openclaw gateway restart"
1224
- );
1225
- }
1226
-
1227
1174
  ensureAgentsDefaultsSchemaCompat(config);
1228
1175
  mkdirSync(CONFIG_DIR, { recursive: true });
1229
1176
  writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
@@ -1231,6 +1178,21 @@ function configureGatewayScheduling(modeConfig, configPath = CONFIG_FILE, option
1231
1178
  const cronStorePath = resolveCronJobsStorePath(config);
1232
1179
  const cronMerge = mergeTraderCronJobsIntoStore(cronStorePath, targetJobs);
1233
1180
 
1181
+ let qmdAvailable = false;
1182
+ let qmdVersion = null;
1183
+ try { qmdAvailable = commandExists("qmd"); } catch {}
1184
+ if (qmdAvailable) {
1185
+ qmdVersion = getCommandOutput("qmd --version");
1186
+ } else {
1187
+ if (typeof console !== "undefined") {
1188
+ console.warn(
1189
+ "[traderclaw] QMD binary not found. Memory engine will fall back to SQLite (no vector search, no temporal decay, no MMR).\n" +
1190
+ "Install QMD: npm install -g @tobilu/qmd\n" +
1191
+ "Then restart the gateway: openclaw gateway restart"
1192
+ );
1193
+ }
1194
+ }
1195
+
1234
1196
  return {
1235
1197
  configPath,
1236
1198
  agentsConfigured: targetAgents.length,
@@ -1242,227 +1204,12 @@ function configureGatewayScheduling(modeConfig, configPath = CONFIG_FILE, option
1242
1204
  cronJobsStoreError: cronMerge.error,
1243
1205
  removedLegacyCronJobs,
1244
1206
  hooksConfigured: config.hooks.mappings.length,
1207
+ qmdAvailable,
1208
+ qmdVersion,
1245
1209
  isV2,
1246
1210
  };
1247
1211
  }
1248
1212
 
1249
- /**
1250
- * Persist the optional Telegram forward recipient onto the plugin entry config so the
1251
- * orchestrator's reply-forward path works on first install. Idempotent — does nothing
1252
- * when no recipient is provided. Accepts numeric chat ids (e.g. "818973873") or
1253
- * @username (e.g. "@neabi").
1254
- */
1255
- function writeForwardTelegramRecipient(modeConfig, recipient, configPath = CONFIG_FILE) {
1256
- if (typeof recipient !== "string" || !recipient.trim()) return { written: false };
1257
- let config = {};
1258
- try {
1259
- config = JSON.parse(readFileSync(configPath, "utf-8"));
1260
- } catch {
1261
- config = {};
1262
- }
1263
- if (!config.plugins || typeof config.plugins !== "object") config.plugins = {};
1264
- if (!config.plugins.entries || typeof config.plugins.entries !== "object") config.plugins.entries = {};
1265
- normalizeTraderPluginEntries(config, modeConfig.pluginId);
1266
- const entry = config.plugins.entries[modeConfig.pluginId];
1267
- if (!entry || typeof entry !== "object") return { written: false };
1268
- if (!entry.config || typeof entry.config !== "object") entry.config = {};
1269
- entry.config.forwardTelegramRecipient = recipient.trim();
1270
- ensureAgentsDefaultsSchemaCompat(config);
1271
- mkdirSync(CONFIG_DIR, { recursive: true });
1272
- writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1273
- return { written: true, recipient: entry.config.forwardTelegramRecipient };
1274
- }
1275
-
1276
- /**
1277
- * Approve any pending OpenClaw-paired devices on this host. Newly-paired devices
1278
- * land with only `operator.read` scope; new beta plugin RPCs need `operator.write` +
1279
- * `approvals` + `secrets`, so without approval the CLI loops on `1008 pairing required`.
1280
- *
1281
- * Best-effort: tolerates missing `openclaw devices` subcommand and an empty pending list.
1282
- * Returns counts but never throws — callers wrap in try/catch.
1283
- */
1284
- async function approvePendingDevices() {
1285
- if (!commandExists("openclaw")) return { ran: false, reason: "no_cli" };
1286
- let listOut = "";
1287
- try {
1288
- listOut = execFileSync("openclaw", ["devices", "list", "--json"], {
1289
- encoding: "utf-8",
1290
- timeout: 15_000,
1291
- env: NO_COLOR_ENV,
1292
- }).trim();
1293
- } catch (err) {
1294
- return { ran: false, reason: "list_failed", error: err?.message || String(err) };
1295
- }
1296
- if (!listOut) return { ran: true, approved: 0, total: 0 };
1297
-
1298
- let parsed;
1299
- try {
1300
- parsed = JSON.parse(listOut);
1301
- } catch {
1302
- return { ran: true, approved: 0, total: 0, reason: "list_not_json" };
1303
- }
1304
- const devices = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.devices) ? parsed.devices : [];
1305
-
1306
- // Devices needing operator action: pending pair, awaiting approval, or flagged for repair
1307
- // (repair is what existing devices get when new RPC scopes are introduced — see VPS post-mortem).
1308
- const PENDING_STATES = new Set(["pending", "awaiting_approval", "needs_approval", "repair", "scope_repair"]);
1309
- const allCandidates = devices.filter(
1310
- (d) => d && typeof d === "object" && (PENDING_STATES.has(String(d.status || "").toLowerCase()) || d.requiresApproval === true),
1311
- );
1312
-
1313
- // Authorization-scope safety: only auto-approve devices that we can prove belong to
1314
- // this host. Approving unrelated pending devices would silently grant the new
1315
- // operator.write/approvals/secrets scopes to someone else's machine. Match strategies:
1316
- // 1) device.hostname / device.host equals os.hostname()
1317
- // 2) device.deviceId / device.fingerprint equals a fingerprint we just paired (env)
1318
- // 3) operator opted in to broad approval via TRADERCLAW_INSTALLER_APPROVE_ALL_PENDING=1
1319
- // If none match and there is more than one pending device, do nothing and surface a
1320
- // log line so the operator can run `openclaw devices approve <id>` themselves.
1321
- const myHost = String(os.hostname() || "").toLowerCase();
1322
- const justPairedId = String(process.env.OPENCLAW_JUST_PAIRED_DEVICE_ID || "").trim();
1323
- const optInBroad = process.env.TRADERCLAW_INSTALLER_APPROVE_ALL_PENDING === "1";
1324
-
1325
- const matchesThisHost = (d) => {
1326
- const dh = String(d.hostname || d.host || "").toLowerCase();
1327
- const did = String(d.id || d.deviceId || d.fingerprint || "").trim();
1328
- if (myHost && dh && dh === myHost) return true;
1329
- if (justPairedId && did && did === justPairedId) return true;
1330
- return false;
1331
- };
1332
-
1333
- let candidates = allCandidates.filter(matchesThisHost);
1334
- // Conservative fallback: if exactly one pending candidate exists overall and no host
1335
- // metadata is available to disambiguate, treat it as "the device we just paired".
1336
- if (candidates.length === 0 && allCandidates.length === 1) {
1337
- candidates = allCandidates;
1338
- }
1339
- if (optInBroad) {
1340
- candidates = allCandidates;
1341
- }
1342
-
1343
- if (candidates.length === 0 && allCandidates.length > 1) {
1344
- return {
1345
- ran: true,
1346
- approved: 0,
1347
- total: devices.length,
1348
- candidates: 0,
1349
- pending: allCandidates.length,
1350
- reason: "ambiguous_pending_devices",
1351
- pendingIds: allCandidates.map((d) => String(d.id || d.deviceId || "")).filter(Boolean),
1352
- };
1353
- }
1354
-
1355
- let approved = 0;
1356
- const errors = [];
1357
- for (const d of candidates) {
1358
- const id = String(d.id || d.deviceId || "").trim();
1359
- if (!id) continue;
1360
- try {
1361
- execFileSync("openclaw", ["devices", "approve", id], {
1362
- encoding: "utf-8",
1363
- timeout: 15_000,
1364
- env: NO_COLOR_ENV,
1365
- });
1366
- approved += 1;
1367
- } catch (err) {
1368
- errors.push({ id, error: err?.message || String(err) });
1369
- }
1370
- }
1371
- return { ran: true, approved, total: devices.length, candidates: candidates.length, errors };
1372
- }
1373
-
1374
- /**
1375
- * Final invariant check before declaring install complete. Re-reads openclaw.json from
1376
- * disk and verifies every default that the install promised to ship correctly. Throws
1377
- * (failing the wizard) on any mismatch so a broken config never ships to a fresh user.
1378
- */
1379
- function runFreshInstallSmokeCheck(modeConfig, options, configPath = CONFIG_FILE) {
1380
- const issues = [];
1381
- let config;
1382
- try {
1383
- config = JSON.parse(readFileSync(configPath, "utf-8"));
1384
- } catch (err) {
1385
- return { ok: false, issues: [`Could not read ${configPath}: ${err?.message || String(err)}`] };
1386
- }
1387
-
1388
- // 1. Telegram dmPolicy must be "open" (only relevant when telegram was enabled)
1389
- if (options?.enableTelegram === true) {
1390
- const dmPolicy = config?.channels?.telegram?.dmPolicy;
1391
- if (dmPolicy !== "open") {
1392
- issues.push(`channels.telegram.dmPolicy is '${dmPolicy ?? "unset"}' (expected 'open' for wizard installs).`);
1393
- }
1394
- }
1395
-
1396
- // 2. Every agent's heartbeat.target must be set to *something* routable. The fresh-install
1397
- // default is "last", but a reinstall over a config where the operator intentionally chose
1398
- // "@username" or "telegram:<id>" must NOT fail — configureGatewayScheduling preserves
1399
- // those values, so failing here would block legitimate reinstalls. Only fail when the
1400
- // agent has heartbeat config but no target at all (the regression we kept hitting).
1401
- const agents = Array.isArray(config?.agents?.list) ? config.agents.list : [];
1402
- for (const a of agents) {
1403
- if (!a?.heartbeat) continue;
1404
- const t = a.heartbeat.target;
1405
- if (!t) {
1406
- issues.push(`agents.list[id=${a.id}].heartbeat is configured but heartbeat.target is missing (expected 'last' or a routable target like '@username' / 'telegram:<id>').`);
1407
- }
1408
- }
1409
-
1410
- // 3. Every X profile must have userId+username (only when X consumer keys are configured)
1411
- const entry = config?.plugins?.entries?.[modeConfig.pluginId];
1412
- const xCfg = entry?.config?.x;
1413
- if (xCfg && xCfg.consumerKey && xCfg.consumerSecret && xCfg.profiles && typeof xCfg.profiles === "object") {
1414
- for (const [pid, p] of Object.entries(xCfg.profiles)) {
1415
- if (!p || typeof p !== "object") continue;
1416
- if (!p.accessToken || !p.accessTokenSecret) continue;
1417
- if (!p.userId || !p.username) {
1418
- issues.push(`plugins.entries.${modeConfig.pluginId}.config.x.profiles.${pid} missing userId or username (x_read_mentions will fail).`);
1419
- }
1420
- }
1421
- }
1422
-
1423
- // 4. Every cron job's model must match the wizard provider
1424
- const wantProvider = String(options?.llmProvider || "").trim();
1425
- if (wantProvider) {
1426
- let cronStorePath = "";
1427
- try {
1428
- cronStorePath = resolveCronJobsStorePath(config);
1429
- } catch {
1430
- cronStorePath = "";
1431
- }
1432
- if (cronStorePath && existsSync(cronStorePath)) {
1433
- let store = null;
1434
- try {
1435
- store = JSON.parse(readFileSync(cronStorePath, "utf-8"));
1436
- } catch (err) {
1437
- issues.push(`cron store '${cronStorePath}' could not be parsed: ${err?.message || String(err)} — provider/model alignment cannot be verified.`);
1438
- }
1439
- if (store) {
1440
- // Only enforce provider alignment on installer-managed TraderClaw jobs. Custom
1441
- // jobs the operator added between installs (or other plugins' jobs) are intentionally
1442
- // preserved by mergeTraderCronJobsIntoStore and must not fail the smoke check.
1443
- const managedIds = new Set([
1444
- ...traderCronPrescriptiveJobs("main").map((j) => j.id),
1445
- ...traderCronPrescriptiveJobs("cto").map((j) => j.id),
1446
- ].filter(Boolean));
1447
- const jobs = Array.isArray(store?.jobs) ? store.jobs : [];
1448
- for (const j of jobs) {
1449
- if (!j || typeof j !== "object") continue;
1450
- if (!managedIds.has(j.id)) continue;
1451
- // OpenClaw cron store nests model under job.payload.model (see buildOpenClawCronStoreJob).
1452
- // Fall back to top-level j.model only as a defensive read in case the schema changes.
1453
- const m = String(j?.payload?.model || j?.model || "");
1454
- if (!m) continue;
1455
- if (!m.startsWith(`${wantProvider}/`)) {
1456
- issues.push(`installer-managed cron job '${j.id}' has model '${m}' but wizard provider is '${wantProvider}'.`);
1457
- }
1458
- }
1459
- }
1460
- }
1461
- }
1462
-
1463
- return { ok: issues.length === 0, issues, configPath };
1464
- }
1465
-
1466
1213
  function ensureOpenResponsesEnabled(configPath = CONFIG_FILE) {
1467
1214
  let config = {};
1468
1215
  try {
@@ -1626,40 +1373,19 @@ function seedXConfig(modeConfig, configPath = CONFIG_FILE, wizardOpts = {}) {
1626
1373
  entry.config.x.profiles = {};
1627
1374
  }
1628
1375
 
1629
- // V1 historically only seeded { main, solana-trader }. Production traffic resolves the X
1630
- // profile via callerAgentId || requestedAgentId || fallbackAgentId || "cto" — so a missing
1631
- // "cto" profile (the literal string-fallback) breaks x_read_mentions for any caller that
1632
- // doesn't explicitly pass agentId. Always seed "cto" for V1 too, mirroring "main" tokens
1633
- // when no dedicated cto credentials were supplied.
1634
- // Additionally, mirror under any agents.list[].identity.name (e.g. "AgentZERO") so any
1635
- // identity-name-based resolution path lands on a real profile.
1636
- const baseAgentIds =
1376
+ const agentIds =
1637
1377
  modeConfig.pluginId === "solana-trader-v2"
1638
1378
  ? ["cto", "intern"]
1639
1379
  : modeConfig.pluginId === "solana-trader"
1640
- ? ["main", "solana-trader", "cto"]
1380
+ ? ["main", "solana-trader"]
1641
1381
  : ["main"];
1642
-
1643
- const identityNames = new Set();
1644
- if (Array.isArray(config?.agents?.list)) {
1645
- for (const a of config.agents.list) {
1646
- const n = typeof a?.identity?.name === "string" ? a.identity.name.trim() : "";
1647
- if (n && !baseAgentIds.includes(n)) identityNames.add(n);
1648
- }
1649
- }
1650
- const agentIds = [...baseAgentIds, ...identityNames];
1651
-
1652
1382
  let profilesFound = 0;
1653
1383
 
1654
1384
  for (const agentId of agentIds) {
1655
1385
  let { at, ats } = getAccessPairForAgent(wizardOpts, agentId);
1656
- // Mirror "main" tokens for any V1 secondary slot (solana-trader, cto, AgentZERO, …)
1657
- // when no dedicated tokens were supplied. Production X tools are stateless w.r.t.
1658
- // which token issued the call, so reusing "main" tokens across profiles is safe and
1659
- // matches what the user manually does on every install.
1660
1386
  if (
1661
1387
  modeConfig.pluginId === "solana-trader"
1662
- && agentId !== "main"
1388
+ && agentId === "solana-trader"
1663
1389
  && (!at || !ats)
1664
1390
  ) {
1665
1391
  ({ at, ats } = getAccessPairForAgent(wizardOpts, "main"));
@@ -2171,9 +1897,9 @@ function verifyInstallation(modeConfig, apiKey) {
2171
1897
  note: heartbeatInWorkspace ? workspaceRoot : `expected ${join(workspaceRoot, "HEARTBEAT.md")}`,
2172
1898
  },
2173
1899
  {
2174
- label: "Memory engine (builtin, vector + FTS)",
2175
- ok: true,
2176
- note: "run `openclaw memory status --deep` to verify index",
1900
+ label: "QMD memory engine (vector search)",
1901
+ ok: commandExists("qmd"),
1902
+ note: "not installed memory uses keyword search only. Install: npm install -g @tobilu/qmd",
2177
1903
  },
2178
1904
  ];
2179
1905
  }
@@ -2209,13 +1935,6 @@ export class InstallerStepEngine {
2209
1935
  gatewayToken: options.gatewayToken || "",
2210
1936
  enableTelegram: options.enableTelegram === true,
2211
1937
  telegramToken: options.telegramToken || "",
2212
- // Optional: numeric chat id or @username the orchestrator forwards Telegram replies to.
2213
- // When set, written to plugins.entries[<pluginId>].config.forwardTelegramRecipient so the
2214
- // forward path works on first install without a manual edit. Legacy alias accepted.
2215
- forwardTelegramRecipient:
2216
- typeof options.forwardTelegramRecipient === "string" ? options.forwardTelegramRecipient.trim()
2217
- : typeof options.forwardTelegramChatId === "string" ? options.forwardTelegramChatId.trim()
2218
- : "",
2219
1938
  autoInstallDeps: options.autoInstallDeps !== false,
2220
1939
  skipPreflight: options.skipPreflight === true,
2221
1940
  skipInstallOpenClaw: options.skipInstallOpenClaw === true,
@@ -2463,17 +2182,6 @@ export class InstallerStepEngine {
2463
2182
  writeTelegramChannelConfig(this.options.telegramToken, CONFIG_FILE);
2464
2183
  this.emitLog("telegram_required", "info", "Telegram bot token written to openclaw.json (channels.telegram.botToken).");
2465
2184
 
2466
- if (this.options.forwardTelegramRecipient) {
2467
- const fwd = writeForwardTelegramRecipient(this.modeConfig, this.options.forwardTelegramRecipient, CONFIG_FILE);
2468
- if (fwd.written) {
2469
- this.emitLog(
2470
- "telegram_required",
2471
- "info",
2472
- `Forward recipient written to plugins.entries.${this.modeConfig.pluginId}.config.forwardTelegramRecipient = ${fwd.recipient}.`,
2473
- );
2474
- }
2475
- }
2476
-
2477
2185
  const policy = ensureTelegramGroupPolicyOpenForWizard();
2478
2186
  if (policy.changed) {
2479
2187
  this.emitLog(
@@ -2675,6 +2383,40 @@ export class InstallerStepEngine {
2675
2383
  if (!this.options.skipInstallOpenClaw) {
2676
2384
  await this.runStep("install_openclaw", "Installing or upgrading OpenClaw platform", async () => installOpenClawPlatform());
2677
2385
  }
2386
+
2387
+ // Non-fatal: warn when the CLI has devices in pending-approval or repair state.
2388
+ // Gateway >= 1.0.93-beta.0 requires explicit device approval for operator-write scope;
2389
+ // without it, agent trading RPCs silently fail (device gets read-only "repair" state).
2390
+ await this.runStep("device_approval_check", "Checking OpenClaw device approval status", async () => {
2391
+ const check = checkOpenClawDeviceApproval();
2392
+ if (!check.ran) {
2393
+ this.emitLog("device_approval_check", "info", "Device approval check skipped (openclaw CLI not available or devices subcommand not supported).");
2394
+ return { ran: false };
2395
+ }
2396
+ const needsAction = check.pendingIds.length > 0 || check.repairDetected;
2397
+ if (!needsAction) {
2398
+ this.emitLog("device_approval_check", "info", "No pending or repair-state devices found. Device approval OK.");
2399
+ return { ran: true, ok: true };
2400
+ }
2401
+ const lines = [
2402
+ "ACTION REQUIRED — OpenClaw device approval needed.",
2403
+ "The gateway requires explicit device approval for operator-write scope.",
2404
+ "Without it, trading RPCs will fail silently (read-only / repair state).",
2405
+ "",
2406
+ "Run in your VPS shell:",
2407
+ " openclaw devices list",
2408
+ ...(check.pendingIds.length > 0
2409
+ ? check.pendingIds.map((id) => ` openclaw devices approve ${id}`)
2410
+ : [" openclaw devices approve <requestId> # use the id shown above"]),
2411
+ "",
2412
+ check.envTokenSet
2413
+ ? "OPENCLAW_GATEWAY_TOKEN env var is already set — env-first auth will work as a fallback."
2414
+ : "Optionally set: export OPENCLAW_GATEWAY_TOKEN=\"<token>\" # bypasses device auth entirely",
2415
+ ];
2416
+ this.emitLog("device_approval_check", "warn", lines.join("\n"));
2417
+ return { ran: true, ok: false, pendingIds: check.pendingIds, repairDetected: check.repairDetected, envTokenSet: check.envTokenSet };
2418
+ });
2419
+
2678
2420
  await this.runStep("configure_llm", "Configuring required OpenClaw LLM provider", async () => this.configureLlmStep());
2679
2421
  if (!this.options.skipInstallPlugin) {
2680
2422
  await this.runStep("install_plugin_package", "Installing TraderClaw CLI package", async () =>
@@ -2685,6 +2427,32 @@ export class InstallerStepEngine {
2685
2427
  await this.runStep("openclaw_global_deps", "Ensuring OpenClaw global package dependencies", async () =>
2686
2428
  ensureOpenClawGlobalPackageDependencies(),
2687
2429
  );
2430
+ await this.runStep("install_qmd", "Installing QMD memory engine (vector search)", async () => {
2431
+ if (commandExists("qmd")) {
2432
+ const ver = getCommandOutput("qmd --version");
2433
+ this.emitLog("install_qmd", "info", `QMD already installed: ${ver}`);
2434
+ return { alreadyInstalled: true, version: ver };
2435
+ }
2436
+ this.emitLog("install_qmd", "info", "Installing @tobilu/qmd globally for vector search memory...");
2437
+ try {
2438
+ await runCommandWithEvents("npm", ["install", "-g", "--ignore-scripts", "--registry", "https://registry.npmjs.org/", "@tobilu/qmd"], {
2439
+ onEvent: (evt) => this.emitLog("install_qmd", evt.type === "stderr" ? "warn" : "info", evt.text, evt.urls || []),
2440
+ });
2441
+ } catch (err) {
2442
+ this.emitLog(
2443
+ "install_qmd",
2444
+ "warn",
2445
+ `QMD install failed (non-fatal): ${err?.message || err}. Memory will use keyword search only. You can install manually later: npm install -g @tobilu/qmd`,
2446
+ );
2447
+ return { installed: false, error: err?.message || String(err) };
2448
+ }
2449
+ const available = commandExists("qmd");
2450
+ const ver = available ? getCommandOutput("qmd --version") : null;
2451
+ if (!available) {
2452
+ this.emitLog("install_qmd", "warn", "QMD installed but not on PATH. Memory will use keyword search only.");
2453
+ }
2454
+ return { installed: available, version: ver };
2455
+ });
2688
2456
  await this.runStep(
2689
2457
  "activate_openclaw_plugin",
2690
2458
  "Installing and enabling TraderClaw inside OpenClaw",
@@ -2762,33 +2530,6 @@ export class InstallerStepEngine {
2762
2530
  runPrivileged: (cmd, args) => this.runWithPrivilegeGuidance("gateway_persistence", cmd, args),
2763
2531
  });
2764
2532
  });
2765
-
2766
- // Best-effort: approve the device that was just paired by the wizard so the new
2767
- // beta-RPC scopes (operator.write/approvals/secrets) are granted before the user
2768
- // hits a tool. Tolerates missing CLI subcommands; never fails the install.
2769
- await this.runStep("device_approval", "Approving freshly paired device", async () => {
2770
- try {
2771
- const result = await approvePendingDevices();
2772
- if (!result.ran) {
2773
- this.emitLog("device_approval", "info", `Skipped (${result.reason || "unknown"}). Approve manually with 'openclaw devices approve <id>' if RPC calls return 1008.`);
2774
- return { skipped: true, reason: result.reason };
2775
- }
2776
- if (result.approved > 0) {
2777
- this.emitLog("device_approval", "info", `Approved ${result.approved}/${result.candidates ?? result.approved} pending device(s) (of ${result.total} total).`);
2778
- } else {
2779
- this.emitLog("device_approval", "info", `No devices required approval (${result.total} total).`);
2780
- }
2781
- if (Array.isArray(result.errors) && result.errors.length > 0) {
2782
- for (const e of result.errors) {
2783
- this.emitLog("device_approval", "warn", `Could not approve device ${e.id}: ${e.error}`);
2784
- }
2785
- }
2786
- return result;
2787
- } catch (err) {
2788
- this.emitLog("device_approval", "warn", `Device approval skipped: ${err?.message || String(err)}. Approve manually with 'openclaw devices approve <id>' if RPC calls return 1008.`);
2789
- return { skipped: true, error: err?.message || String(err) };
2790
- }
2791
- });
2792
2533
  }
2793
2534
 
2794
2535
  await this.runStep("enable_responses", "Enabling /v1/responses endpoint", async () => {
@@ -2798,7 +2539,7 @@ export class InstallerStepEngine {
2798
2539
  });
2799
2540
 
2800
2541
  await this.runStep("gateway_scheduling", "Configuring heartbeat and cron schedules", async () => {
2801
- const result = configureGatewayScheduling(this.modeConfig, CONFIG_FILE, this.options);
2542
+ const result = configureGatewayScheduling(this.modeConfig, CONFIG_FILE);
2802
2543
  this.emitLog("gateway_scheduling", "info", `Agents configured: ${result.agentsConfigured}`);
2803
2544
  if (result.cronJobsStoreWriteOk) {
2804
2545
  this.emitLog(
@@ -2819,6 +2560,17 @@ export class InstallerStepEngine {
2819
2560
  this.emitLog("gateway_scheduling", "warn", "Removed legacy 'cron.jobs' from openclaw.json to keep config validation compatible.");
2820
2561
  }
2821
2562
  this.emitLog("gateway_scheduling", "info", `Webhook hooks: ${result.hooksConfigured}`);
2563
+ if (!result.qmdAvailable) {
2564
+ this.emitLog(
2565
+ "gateway_scheduling",
2566
+ "warn",
2567
+ "QMD binary not found — memory will use SQLite keyword search only (no vector search, no temporal decay, no MMR). " +
2568
+ "Vector search makes the agent's memory significantly more effective. " +
2569
+ "Install: npm install -g @tobilu/qmd — then restart the gateway: openclaw gateway restart",
2570
+ );
2571
+ } else {
2572
+ this.emitLog("gateway_scheduling", "info", `QMD memory engine: ${result.qmdVersion || "installed"}`);
2573
+ }
2822
2574
  const restart = await restartGateway();
2823
2575
  return { ...result, restart };
2824
2576
  });
@@ -2876,71 +2628,23 @@ export class InstallerStepEngine {
2876
2628
  this.emitLog("x_credentials", "warn", `Missing X profiles for: ${missing.join(", ")}. Set tokens in the wizard or X_ACCESS_TOKEN_<AGENT_ID> / X_ACCESS_TOKEN_<AGENT_ID>_SECRET env vars.`);
2877
2629
  }
2878
2630
  const { consumerKey, consumerSecret } = getConsumerKeysFromWizard(this.options);
2879
- // Group agentIds by unique (accessToken, accessTokenSecret) pair so we only call
2880
- // GET /2/users/me once per real X account, then apply the resolved userId+username
2881
- // to every profile that uses that token pair. Previously the loop verified per-agent
2882
- // and silently skipped persisting userId on profiles whose verify call had transient
2883
- // failures — leaving x_read_mentions broken on those agents.
2884
- const pairGroups = new Map();
2885
- const pairOwner = new Map();
2886
- const isV1 = this.modeConfig.pluginId === "solana-trader";
2887
- for (const agentId of result.agentIds) {
2888
- let { at, ats } = getAccessPairForAgent(this.options, agentId);
2889
- // Mirror seedXConfig fallback: V1 secondary slots reuse main's tokens when no
2890
- // dedicated pair was supplied. Without this, mirrored profiles (cto / solana-trader /
2891
- // identity-name agents) get seeded with main tokens but skipped here, leaving their
2892
- // userId/username unpopulated and breaking x_read_mentions on those agents.
2893
- if (isV1 && agentId !== "main" && (!at || !ats)) {
2894
- ({ at, ats } = getAccessPairForAgent(this.options, "main"));
2895
- }
2896
- if (!at || !ats) continue;
2897
- const key = `${at}::${ats}`;
2898
- if (!pairGroups.has(key)) {
2899
- pairGroups.set(key, { at, ats, agentIds: [] });
2900
- pairOwner.set(key, agentId);
2901
- }
2902
- pairGroups.get(key).agentIds.push(agentId);
2903
- }
2904
-
2905
2631
  const verified = [];
2906
2632
  const identitiesToPersist = [];
2907
- for (const [key, group] of pairGroups) {
2908
- const owner = pairOwner.get(key);
2909
- let check = null;
2910
- let lastErr = null;
2911
- for (let attempt = 1; attempt <= 2; attempt++) {
2633
+ for (const agentId of result.agentIds) {
2634
+ const { at, ats } = getAccessPairForAgent(this.options, agentId);
2635
+ if (at && ats) {
2912
2636
  try {
2913
- check = await verifyXCredentials(consumerKey, consumerSecret, group.at, group.ats);
2914
- if (check.ok) break;
2915
- lastErr = `HTTP ${check.status}`;
2916
- if (attempt < 2) {
2917
- this.emitLog("x_credentials", "warn", `Verify failed for token-pair owner '${owner}' (HTTP ${check.status}); retrying once.`);
2918
- await new Promise(r => setTimeout(r, 1500));
2637
+ const check = await verifyXCredentials(consumerKey, consumerSecret, at, ats);
2638
+ if (check.ok) {
2639
+ this.emitLog("x_credentials", "info", `Verified X profile '${agentId}': @${check.username} (${check.userId})`);
2640
+ verified.push({ agentId, username: check.username, userId: check.userId });
2641
+ identitiesToPersist.push({ agentId, userId: check.userId, username: check.username });
2642
+ } else {
2643
+ this.emitLog("x_credentials", "warn", `X credential verification failed for '${agentId}': HTTP ${check.status}`);
2919
2644
  }
2920
2645
  } catch (err) {
2921
- lastErr = err?.message || String(err);
2922
- if (attempt < 2) {
2923
- this.emitLog("x_credentials", "warn", `Verify error for token-pair owner '${owner}' (${lastErr}); retrying once.`);
2924
- await new Promise(r => setTimeout(r, 1500));
2925
- }
2926
- }
2927
- }
2928
- if (check && check.ok && check.userId && check.username) {
2929
- this.emitLog(
2930
- "x_credentials",
2931
- "info",
2932
- `Verified X token-pair (owner '${owner}'): @${check.username} (${check.userId}) → applying to ${group.agentIds.length} profile(s): ${group.agentIds.join(", ")}`,
2933
- );
2934
- for (const agentId of group.agentIds) {
2935
- verified.push({ agentId, username: check.username, userId: check.userId });
2936
- identitiesToPersist.push({ agentId, userId: check.userId, username: check.username });
2646
+ this.emitLog("x_credentials", "warn", `X credential verification error for '${agentId}': ${err?.message || String(err)}`);
2937
2647
  }
2938
- } else {
2939
- this.emitLog(
2940
- "x_credentials",
2941
- "warn",
2942
- `X credential verification failed after retry for token-pair owner '${owner}' (${lastErr}). Profiles ${group.agentIds.join(", ")} will not have userId/username persisted; x_read_mentions will fail until tokens are corrected.`,
2943
- );
2944
2648
  }
2945
2649
  }
2946
2650
  if (identitiesToPersist.length > 0) {
@@ -2959,25 +2663,6 @@ export class InstallerStepEngine {
2959
2663
  return { checks };
2960
2664
  });
2961
2665
 
2962
- // Final invariant check: re-read openclaw.json and confirm every default the
2963
- // wizard promised actually landed on disk. Catches regressions where a later
2964
- // step silently overwrites an earlier one (the class of bug behind every manual
2965
- // VPS fix in the post-mortem). Fails the install rather than shipping a broken
2966
- // config to the user.
2967
- await this.runStep("final_smoke_check", "Verifying fresh-install invariants", async () => {
2968
- const result = runFreshInstallSmokeCheck(this.modeConfig, this.options, CONFIG_FILE);
2969
- if (!result.ok) {
2970
- for (const issue of result.issues) {
2971
- this.emitLog("final_smoke_check", "warn", issue);
2972
- }
2973
- throw new Error(
2974
- `Fresh-install smoke check failed (${result.issues.length} issue${result.issues.length === 1 ? "" : "s"}). The bot will not work out of the box. See warnings above.`,
2975
- );
2976
- }
2977
- this.emitLog("final_smoke_check", "info", "All fresh-install invariants OK (dmPolicy=open, heartbeat.target=last, X profiles have userId/username, cron models match provider).");
2978
- return result;
2979
- });
2980
-
2981
2666
  this.state.status = "completed";
2982
2667
  this.state.completedAt = nowIso();
2983
2668
  return this.state;
@@ -4169,6 +4169,7 @@ Commands:
4169
4169
  status Check connection health and wallet status
4170
4170
  config View and manage configuration
4171
4171
  test-session Test session auth flow (refresh, rotation, challenge) without reinstalling
4172
+ update Update to the latest version and restart the gateway
4172
4173
 
4173
4174
  Setup options:
4174
4175
  --api-key, -k API key (skip interactive prompt)
@@ -4236,9 +4237,59 @@ Examples:
4236
4237
  traderclaw config set apiTimeout 60000
4237
4238
  traderclaw test-session
4238
4239
  traderclaw test-session --wallet-private-key <base58_key>
4240
+ traderclaw update
4241
+ traderclaw update --beta
4239
4242
  `);
4240
4243
  }
4241
4244
 
4245
+ async function cmdUpdate(args) {
4246
+ const tag = args.includes("--beta") ? "beta" : "latest";
4247
+
4248
+ print("\nTraderClaw — Update\n");
4249
+ print("=".repeat(45));
4250
+
4251
+ let currentVersion = "unknown";
4252
+ try {
4253
+ const out = execSync("npm list -g traderclaw-cli --json --depth=0", { encoding: "utf-8" });
4254
+ const data = JSON.parse(out);
4255
+ currentVersion = data?.dependencies?.["traderclaw-cli"]?.version ?? "unknown";
4256
+ } catch {}
4257
+ printInfo(` Current version: ${currentVersion}`);
4258
+
4259
+ let latestVersion = "unknown";
4260
+ try {
4261
+ latestVersion = execSync(`npm view traderclaw-cli@${tag} version`, { encoding: "utf-8" }).trim();
4262
+ } catch {}
4263
+ printInfo(` Available (${tag}):${" ".repeat(Math.max(1, 9 - tag.length))}${latestVersion}`);
4264
+
4265
+ if (currentVersion !== "unknown" && latestVersion !== "unknown" && currentVersion === latestVersion) {
4266
+ printSuccess(`\n Already on the ${tag} version (${currentVersion}). Nothing to do.\n`);
4267
+ return;
4268
+ }
4269
+
4270
+ print(`\n Installing traderclaw-cli@${tag}...\n`);
4271
+ try {
4272
+ execSync(`npm install -g traderclaw-cli@${tag}`, { stdio: "inherit" });
4273
+ } catch {
4274
+ printError("npm install failed. Try running manually:");
4275
+ print(` npm install -g traderclaw-cli@${tag}`);
4276
+ process.exit(1);
4277
+ }
4278
+
4279
+ printSuccess(`\n Package updated.`);
4280
+ print("\n Restarting gateway...\n");
4281
+
4282
+ try {
4283
+ execSync("openclaw gateway restart", { stdio: "inherit" });
4284
+ printSuccess(" Gateway restarted.");
4285
+ } catch {
4286
+ printWarn(" Gateway restart returned non-zero. Restart manually: openclaw gateway restart");
4287
+ }
4288
+
4289
+ print("\n" + "=".repeat(45));
4290
+ printSuccess("\n Update complete!\n");
4291
+ }
4292
+
4242
4293
  async function cmdRepairOpenclaw() {
4243
4294
  const { ensureOpenClawGlobalPackageDependencies } = await import("./installer-step-engine.mjs");
4244
4295
  printInfo("Repairing global OpenClaw npm dependencies (fixes missing grammy, @buape/carbon, etc.)...");
@@ -4303,6 +4354,9 @@ async function main() {
4303
4354
  case "test-session":
4304
4355
  await cmdTestSession(args.slice(1));
4305
4356
  break;
4357
+ case "update":
4358
+ await cmdUpdate(args.slice(1));
4359
+ break;
4306
4360
  default:
4307
4361
  printError(`Unknown command: ${command}`);
4308
4362
  printHelp();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "traderclaw-cli",
3
- "version": "1.0.95-beta.0",
3
+ "version": "1.0.96",
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.95-beta.0"
20
+ "solana-traderclaw": "^1.0.96"
21
21
  },
22
22
  "keywords": [
23
23
  "traderclaw",