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.
- package/bin/installer-step-engine.mjs +159 -474
- package/bin/openclaw-trader.mjs +54 -0
- package/package.json +2 -2
|
@@ -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 = "
|
|
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 = "
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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: "
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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"
|
|
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
|
|
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: "
|
|
2175
|
-
ok:
|
|
2176
|
-
note: "
|
|
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
|
|
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
|
|
2908
|
-
const
|
|
2909
|
-
|
|
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,
|
|
2914
|
-
if (check.ok)
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
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
|
-
|
|
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;
|
package/bin/openclaw-trader.mjs
CHANGED
|
@@ -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.
|
|
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.
|
|
20
|
+
"solana-traderclaw": "^1.0.96"
|
|
21
21
|
},
|
|
22
22
|
"keywords": [
|
|
23
23
|
"traderclaw",
|