traderclaw-cli 1.0.93 → 1.0.95-beta.0
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/cli.ts +61 -0
- package/bin/installer-step-engine.mjs +474 -159
- package/package.json +2 -2
package/bin/cli.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { createInterface } from "readline";
|
|
|
4
4
|
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
import { homedir } from "os";
|
|
7
|
+
import { spawnSync } from "child_process";
|
|
7
8
|
|
|
8
9
|
const VERSION = "1.0.0";
|
|
9
10
|
const PLUGIN_ID = "solana-trader";
|
|
@@ -611,6 +612,57 @@ async function cmdConfig(subArgs: string[]) {
|
|
|
611
612
|
process.exit(1);
|
|
612
613
|
}
|
|
613
614
|
|
|
615
|
+
async function cmdUpdate(args: string[]) {
|
|
616
|
+
const tag = args.includes("--beta") ? "beta" : "latest";
|
|
617
|
+
|
|
618
|
+
print("\nOpenClaw Trader — Update\n");
|
|
619
|
+
print("=".repeat(45));
|
|
620
|
+
|
|
621
|
+
let currentVersion = "unknown";
|
|
622
|
+
try {
|
|
623
|
+
const r = spawnSync("npm", ["list", "-g", "traderclaw-cli", "--json", "--depth=0"], { encoding: "utf-8" });
|
|
624
|
+
if (r.stdout) {
|
|
625
|
+
const data = JSON.parse(r.stdout) as { dependencies?: Record<string, { version?: string }> };
|
|
626
|
+
currentVersion = data?.dependencies?.["traderclaw-cli"]?.version ?? "unknown";
|
|
627
|
+
}
|
|
628
|
+
} catch {}
|
|
629
|
+
printInfo(` Current version: ${currentVersion}`);
|
|
630
|
+
|
|
631
|
+
let latestVersion = "unknown";
|
|
632
|
+
try {
|
|
633
|
+
const r = spawnSync("npm", ["view", `traderclaw-cli@${tag}`, "version"], { encoding: "utf-8" });
|
|
634
|
+
latestVersion = r.stdout?.trim() ?? "unknown";
|
|
635
|
+
} catch {}
|
|
636
|
+
printInfo(` Available (${tag}):${" ".repeat(Math.max(1, 9 - tag.length))}${latestVersion}`);
|
|
637
|
+
|
|
638
|
+
if (currentVersion !== "unknown" && latestVersion !== "unknown" && currentVersion === latestVersion) {
|
|
639
|
+
printSuccess(`\n Already on the ${tag} version (${currentVersion}). Nothing to do.\n`);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
print(`\n Installing traderclaw-cli@${tag}...\n`);
|
|
644
|
+
|
|
645
|
+
const install = spawnSync("npm", ["install", "-g", `traderclaw-cli@${tag}`], { stdio: "inherit" });
|
|
646
|
+
if (install.status !== 0) {
|
|
647
|
+
printError("npm install failed. Try running manually:");
|
|
648
|
+
print(` npm install -g traderclaw-cli@${tag}`);
|
|
649
|
+
process.exit(1);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
printSuccess(`\n Package updated.`);
|
|
653
|
+
print("\n Restarting gateway...\n");
|
|
654
|
+
|
|
655
|
+
const restart = spawnSync("openclaw", ["gateway", "restart"], { stdio: "inherit" });
|
|
656
|
+
if (restart.status !== 0) {
|
|
657
|
+
printWarn(" Gateway restart returned non-zero. Restart manually: openclaw gateway restart");
|
|
658
|
+
} else {
|
|
659
|
+
printSuccess(" Gateway restarted.");
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
print("\n" + "=".repeat(45));
|
|
663
|
+
printSuccess("\n Update complete!\n");
|
|
664
|
+
}
|
|
665
|
+
|
|
614
666
|
function printHelp() {
|
|
615
667
|
print(`
|
|
616
668
|
OpenClaw Solana Trader CLI v${VERSION}
|
|
@@ -621,6 +673,7 @@ Commands:
|
|
|
621
673
|
setup Set up the plugin (API key, orchestrator, wallet)
|
|
622
674
|
status Check connection health and wallet status
|
|
623
675
|
config View and manage configuration
|
|
676
|
+
update Update to the latest version and restart the gateway
|
|
624
677
|
|
|
625
678
|
Setup options:
|
|
626
679
|
--api-key, -k API key (skip interactive prompt)
|
|
@@ -632,11 +685,16 @@ Config subcommands:
|
|
|
632
685
|
config set <k> <v> Update a configuration value
|
|
633
686
|
config reset Remove plugin configuration
|
|
634
687
|
|
|
688
|
+
Update options:
|
|
689
|
+
--beta Update to the latest beta version instead of stable
|
|
690
|
+
|
|
635
691
|
Examples:
|
|
636
692
|
openclaw-trader setup
|
|
637
693
|
openclaw-trader setup --api-key sk_live_abc123 --url https://api.traderclaw.ai
|
|
638
694
|
openclaw-trader setup --telegram-recipient @MyChannelOrUser
|
|
639
695
|
openclaw-trader status
|
|
696
|
+
openclaw-trader update
|
|
697
|
+
openclaw-trader update --beta
|
|
640
698
|
openclaw-trader config show
|
|
641
699
|
openclaw-trader config set apiTimeout 60000
|
|
642
700
|
`);
|
|
@@ -666,6 +724,9 @@ async function main() {
|
|
|
666
724
|
case "config":
|
|
667
725
|
await cmdConfig(args.slice(1));
|
|
668
726
|
break;
|
|
727
|
+
case "update":
|
|
728
|
+
await cmdUpdate(args.slice(1));
|
|
729
|
+
break;
|
|
669
730
|
default:
|
|
670
731
|
printError(`Unknown command: ${command}`);
|
|
671
732
|
printHelp();
|
|
@@ -2,6 +2,7 @@ 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";
|
|
5
6
|
import { dirname, join } from "path";
|
|
6
7
|
import { resolvePluginPackageRoot } from "./resolve-plugin-root.mjs";
|
|
7
8
|
import { choosePreferredProviderModel } from "./llm-model-preference.mjs";
|
|
@@ -212,7 +213,11 @@ const NO_COLOR_ENV = { ...process.env, NO_COLOR: "1", FORCE_COLOR: "0" };
|
|
|
212
213
|
* The current OpenClaw approach (docs.openclaw.ai/channels/telegram):
|
|
213
214
|
* channels.telegram.botToken = "<token>" → token source
|
|
214
215
|
* channels.telegram.enabled = true → enable the channel
|
|
215
|
-
* channels.telegram.dmPolicy = "
|
|
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.
|
|
216
221
|
*/
|
|
217
222
|
function writeTelegramChannelConfig(botToken, configPath = CONFIG_FILE) {
|
|
218
223
|
let config = {};
|
|
@@ -227,7 +232,7 @@ function writeTelegramChannelConfig(botToken, configPath = CONFIG_FILE) {
|
|
|
227
232
|
config.channels.telegram.botToken = botToken;
|
|
228
233
|
// Only set dmPolicy if not already configured (preserve existing policy on re-installs).
|
|
229
234
|
if (!config.channels.telegram.dmPolicy) {
|
|
230
|
-
config.channels.telegram.dmPolicy = "
|
|
235
|
+
config.channels.telegram.dmPolicy = "open";
|
|
231
236
|
}
|
|
232
237
|
ensureAgentsDefaultsSchemaCompat(config);
|
|
233
238
|
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
@@ -530,42 +535,6 @@ async function installOpenClawPlatform() {
|
|
|
530
535
|
};
|
|
531
536
|
}
|
|
532
537
|
|
|
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
|
-
|
|
569
538
|
function isNpmGlobalBinConflict(err, cliName) {
|
|
570
539
|
const text = `${err?.message || ""}\n${err?.stderr || ""}\n${err?.stdout || ""}`.toLowerCase();
|
|
571
540
|
return (
|
|
@@ -898,13 +867,46 @@ function mergePluginsAllowlist(modeConfig, configPath = CONFIG_FILE) {
|
|
|
898
867
|
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
899
868
|
}
|
|
900
869
|
|
|
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
|
+
|
|
901
900
|
/**
|
|
902
901
|
* Managed cron jobs with prescriptive tool chains (VPS report 2026-03-24).
|
|
903
902
|
* Schedules are staggered (minutes :00 / :15 / :30 / :45) where possible to avoid pile-ups.
|
|
904
903
|
* @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) {
|
|
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;
|
|
908
910
|
return [
|
|
909
911
|
{
|
|
910
912
|
id: "alpha-scan",
|
|
@@ -912,7 +914,7 @@ function traderCronPrescriptiveJobs(agentId) {
|
|
|
912
914
|
agentId,
|
|
913
915
|
message:
|
|
914
916
|
"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.",
|
|
915
|
-
model:
|
|
917
|
+
model: HEAVY,
|
|
916
918
|
thinking: false,
|
|
917
919
|
lightContext: true,
|
|
918
920
|
delivery: { mode: "announce", channel: "last", bestEffort: true },
|
|
@@ -924,7 +926,7 @@ function traderCronPrescriptiveJobs(agentId) {
|
|
|
924
926
|
agentId,
|
|
925
927
|
message:
|
|
926
928
|
"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'.",
|
|
927
|
-
model:
|
|
929
|
+
model: HEAVY,
|
|
928
930
|
thinking: false,
|
|
929
931
|
lightContext: true,
|
|
930
932
|
delivery: { mode: "announce", channel: "last", bestEffort: true },
|
|
@@ -936,7 +938,7 @@ function traderCronPrescriptiveJobs(agentId) {
|
|
|
936
938
|
agentId,
|
|
937
939
|
message:
|
|
938
940
|
"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'.",
|
|
939
|
-
model:
|
|
941
|
+
model: LIGHT,
|
|
940
942
|
thinking: false,
|
|
941
943
|
lightContext: true,
|
|
942
944
|
delivery: { mode: "none" },
|
|
@@ -948,7 +950,7 @@ function traderCronPrescriptiveJobs(agentId) {
|
|
|
948
950
|
agentId,
|
|
949
951
|
message:
|
|
950
952
|
"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'.",
|
|
951
|
-
model:
|
|
953
|
+
model: HEAVY,
|
|
952
954
|
thinking: false,
|
|
953
955
|
lightContext: true,
|
|
954
956
|
delivery: { mode: "announce", channel: "last", bestEffort: true },
|
|
@@ -960,7 +962,7 @@ function traderCronPrescriptiveJobs(agentId) {
|
|
|
960
962
|
agentId,
|
|
961
963
|
message:
|
|
962
964
|
"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'.",
|
|
963
|
-
model:
|
|
965
|
+
model: HEAVY,
|
|
964
966
|
thinking: true,
|
|
965
967
|
lightContext: false,
|
|
966
968
|
delivery: { mode: "announce", channel: "last", bestEffort: true },
|
|
@@ -972,7 +974,7 @@ function traderCronPrescriptiveJobs(agentId) {
|
|
|
972
974
|
agentId,
|
|
973
975
|
message:
|
|
974
976
|
"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.",
|
|
975
|
-
model:
|
|
977
|
+
model: LIGHT,
|
|
976
978
|
thinking: false,
|
|
977
979
|
lightContext: true,
|
|
978
980
|
delivery: { mode: "announce", channel: "last", bestEffort: true },
|
|
@@ -984,7 +986,7 @@ function traderCronPrescriptiveJobs(agentId) {
|
|
|
984
986
|
agentId,
|
|
985
987
|
message:
|
|
986
988
|
"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.",
|
|
987
|
-
model:
|
|
989
|
+
model: HEAVY,
|
|
988
990
|
thinking: false,
|
|
989
991
|
lightContext: false,
|
|
990
992
|
delivery: { mode: "announce", channel: "telegram" },
|
|
@@ -996,7 +998,7 @@ function traderCronPrescriptiveJobs(agentId) {
|
|
|
996
998
|
agentId,
|
|
997
999
|
message:
|
|
998
1000
|
"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'.",
|
|
999
|
-
model:
|
|
1001
|
+
model: HEAVY,
|
|
1000
1002
|
thinking: true,
|
|
1001
1003
|
lightContext: false,
|
|
1002
1004
|
delivery: { mode: "none" },
|
|
@@ -1008,7 +1010,7 @@ function traderCronPrescriptiveJobs(agentId) {
|
|
|
1008
1010
|
agentId,
|
|
1009
1011
|
message:
|
|
1010
1012
|
"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.",
|
|
1011
|
-
model:
|
|
1013
|
+
model: LIGHT,
|
|
1012
1014
|
thinking: false,
|
|
1013
1015
|
lightContext: true,
|
|
1014
1016
|
delivery: { mode: "none" },
|
|
@@ -1020,7 +1022,7 @@ function traderCronPrescriptiveJobs(agentId) {
|
|
|
1020
1022
|
agentId,
|
|
1021
1023
|
message:
|
|
1022
1024
|
"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.",
|
|
1023
|
-
model:
|
|
1025
|
+
model: LIGHT,
|
|
1024
1026
|
thinking: false,
|
|
1025
1027
|
lightContext: true,
|
|
1026
1028
|
delivery: { mode: "announce", channel: "telegram" },
|
|
@@ -1029,7 +1031,7 @@ function traderCronPrescriptiveJobs(agentId) {
|
|
|
1029
1031
|
];
|
|
1030
1032
|
}
|
|
1031
1033
|
|
|
1032
|
-
function configureGatewayScheduling(modeConfig, configPath = CONFIG_FILE) {
|
|
1034
|
+
function configureGatewayScheduling(modeConfig, configPath = CONFIG_FILE, options = {}) {
|
|
1033
1035
|
let config = {};
|
|
1034
1036
|
try {
|
|
1035
1037
|
config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
@@ -1047,9 +1049,12 @@ function configureGatewayScheduling(modeConfig, configPath = CONFIG_FILE) {
|
|
|
1047
1049
|
/** Default periodic wake interval for TraderClaw installs (was 5m; stretched to reduce load). */
|
|
1048
1050
|
const defaultHeartbeatEvery = "30m";
|
|
1049
1051
|
|
|
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.
|
|
1050
1055
|
const defaultHeartbeat = {
|
|
1051
1056
|
every: defaultHeartbeatEvery,
|
|
1052
|
-
target: "
|
|
1057
|
+
target: "last",
|
|
1053
1058
|
isolatedSession: true,
|
|
1054
1059
|
lightContext: true,
|
|
1055
1060
|
prompt: heartbeatPrompt,
|
|
@@ -1079,7 +1084,18 @@ function configureGatewayScheduling(modeConfig, configPath = CONFIG_FILE) {
|
|
|
1079
1084
|
if (existingIds.has(agent.id)) {
|
|
1080
1085
|
const existing = config.agents.list.find(a => a.id === agent.id);
|
|
1081
1086
|
if (agent.heartbeat) {
|
|
1082
|
-
existing
|
|
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
|
+
}
|
|
1083
1099
|
}
|
|
1084
1100
|
if (agent.default) {
|
|
1085
1101
|
existing.default = true;
|
|
@@ -1102,7 +1118,11 @@ function configureGatewayScheduling(modeConfig, configPath = CONFIG_FILE) {
|
|
|
1102
1118
|
const mainAgent = isV2 ? "cto" : "main";
|
|
1103
1119
|
|
|
1104
1120
|
/** Six prescriptive managed jobs (VPS report); v2 assigns the same set to the CTO agent. */
|
|
1105
|
-
const
|
|
1121
|
+
const cronModels = resolveCronModels(
|
|
1122
|
+
String(options?.llmProvider || "anthropic").trim(),
|
|
1123
|
+
String(options?.llmModel || "").trim(),
|
|
1124
|
+
);
|
|
1125
|
+
const targetJobs = traderCronPrescriptiveJobs(mainAgent, cronModels);
|
|
1106
1126
|
|
|
1107
1127
|
let removedLegacyCronJobs = false;
|
|
1108
1128
|
if (config.cron && Object.prototype.hasOwnProperty.call(config.cron, "jobs")) {
|
|
@@ -1171,6 +1191,39 @@ function configureGatewayScheduling(modeConfig, configPath = CONFIG_FILE) {
|
|
|
1171
1191
|
}
|
|
1172
1192
|
config.agents.defaults.heartbeat = { ...defaultHeartbeat };
|
|
1173
1193
|
|
|
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
|
+
|
|
1174
1227
|
ensureAgentsDefaultsSchemaCompat(config);
|
|
1175
1228
|
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
1176
1229
|
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
@@ -1178,21 +1231,6 @@ function configureGatewayScheduling(modeConfig, configPath = CONFIG_FILE) {
|
|
|
1178
1231
|
const cronStorePath = resolveCronJobsStorePath(config);
|
|
1179
1232
|
const cronMerge = mergeTraderCronJobsIntoStore(cronStorePath, targetJobs);
|
|
1180
1233
|
|
|
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
|
-
|
|
1196
1234
|
return {
|
|
1197
1235
|
configPath,
|
|
1198
1236
|
agentsConfigured: targetAgents.length,
|
|
@@ -1204,12 +1242,227 @@ function configureGatewayScheduling(modeConfig, configPath = CONFIG_FILE) {
|
|
|
1204
1242
|
cronJobsStoreError: cronMerge.error,
|
|
1205
1243
|
removedLegacyCronJobs,
|
|
1206
1244
|
hooksConfigured: config.hooks.mappings.length,
|
|
1207
|
-
qmdAvailable,
|
|
1208
|
-
qmdVersion,
|
|
1209
1245
|
isV2,
|
|
1210
1246
|
};
|
|
1211
1247
|
}
|
|
1212
1248
|
|
|
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
|
+
|
|
1213
1466
|
function ensureOpenResponsesEnabled(configPath = CONFIG_FILE) {
|
|
1214
1467
|
let config = {};
|
|
1215
1468
|
try {
|
|
@@ -1373,19 +1626,40 @@ function seedXConfig(modeConfig, configPath = CONFIG_FILE, wizardOpts = {}) {
|
|
|
1373
1626
|
entry.config.x.profiles = {};
|
|
1374
1627
|
}
|
|
1375
1628
|
|
|
1376
|
-
|
|
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 =
|
|
1377
1637
|
modeConfig.pluginId === "solana-trader-v2"
|
|
1378
1638
|
? ["cto", "intern"]
|
|
1379
1639
|
: modeConfig.pluginId === "solana-trader"
|
|
1380
|
-
? ["main", "solana-trader"]
|
|
1640
|
+
? ["main", "solana-trader", "cto"]
|
|
1381
1641
|
: ["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
|
+
|
|
1382
1652
|
let profilesFound = 0;
|
|
1383
1653
|
|
|
1384
1654
|
for (const agentId of agentIds) {
|
|
1385
1655
|
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.
|
|
1386
1660
|
if (
|
|
1387
1661
|
modeConfig.pluginId === "solana-trader"
|
|
1388
|
-
&& agentId
|
|
1662
|
+
&& agentId !== "main"
|
|
1389
1663
|
&& (!at || !ats)
|
|
1390
1664
|
) {
|
|
1391
1665
|
({ at, ats } = getAccessPairForAgent(wizardOpts, "main"));
|
|
@@ -1897,9 +2171,9 @@ function verifyInstallation(modeConfig, apiKey) {
|
|
|
1897
2171
|
note: heartbeatInWorkspace ? workspaceRoot : `expected ${join(workspaceRoot, "HEARTBEAT.md")}`,
|
|
1898
2172
|
},
|
|
1899
2173
|
{
|
|
1900
|
-
label: "
|
|
1901
|
-
ok:
|
|
1902
|
-
note: "
|
|
2174
|
+
label: "Memory engine (builtin, vector + FTS)",
|
|
2175
|
+
ok: true,
|
|
2176
|
+
note: "run `openclaw memory status --deep` to verify index",
|
|
1903
2177
|
},
|
|
1904
2178
|
];
|
|
1905
2179
|
}
|
|
@@ -1935,6 +2209,13 @@ export class InstallerStepEngine {
|
|
|
1935
2209
|
gatewayToken: options.gatewayToken || "",
|
|
1936
2210
|
enableTelegram: options.enableTelegram === true,
|
|
1937
2211
|
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
|
+
: "",
|
|
1938
2219
|
autoInstallDeps: options.autoInstallDeps !== false,
|
|
1939
2220
|
skipPreflight: options.skipPreflight === true,
|
|
1940
2221
|
skipInstallOpenClaw: options.skipInstallOpenClaw === true,
|
|
@@ -2182,6 +2463,17 @@ export class InstallerStepEngine {
|
|
|
2182
2463
|
writeTelegramChannelConfig(this.options.telegramToken, CONFIG_FILE);
|
|
2183
2464
|
this.emitLog("telegram_required", "info", "Telegram bot token written to openclaw.json (channels.telegram.botToken).");
|
|
2184
2465
|
|
|
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
|
+
|
|
2185
2477
|
const policy = ensureTelegramGroupPolicyOpenForWizard();
|
|
2186
2478
|
if (policy.changed) {
|
|
2187
2479
|
this.emitLog(
|
|
@@ -2383,40 +2675,6 @@ export class InstallerStepEngine {
|
|
|
2383
2675
|
if (!this.options.skipInstallOpenClaw) {
|
|
2384
2676
|
await this.runStep("install_openclaw", "Installing or upgrading OpenClaw platform", async () => installOpenClawPlatform());
|
|
2385
2677
|
}
|
|
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
|
-
|
|
2420
2678
|
await this.runStep("configure_llm", "Configuring required OpenClaw LLM provider", async () => this.configureLlmStep());
|
|
2421
2679
|
if (!this.options.skipInstallPlugin) {
|
|
2422
2680
|
await this.runStep("install_plugin_package", "Installing TraderClaw CLI package", async () =>
|
|
@@ -2427,32 +2685,6 @@ export class InstallerStepEngine {
|
|
|
2427
2685
|
await this.runStep("openclaw_global_deps", "Ensuring OpenClaw global package dependencies", async () =>
|
|
2428
2686
|
ensureOpenClawGlobalPackageDependencies(),
|
|
2429
2687
|
);
|
|
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
|
-
});
|
|
2456
2688
|
await this.runStep(
|
|
2457
2689
|
"activate_openclaw_plugin",
|
|
2458
2690
|
"Installing and enabling TraderClaw inside OpenClaw",
|
|
@@ -2530,6 +2762,33 @@ export class InstallerStepEngine {
|
|
|
2530
2762
|
runPrivileged: (cmd, args) => this.runWithPrivilegeGuidance("gateway_persistence", cmd, args),
|
|
2531
2763
|
});
|
|
2532
2764
|
});
|
|
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
|
+
});
|
|
2533
2792
|
}
|
|
2534
2793
|
|
|
2535
2794
|
await this.runStep("enable_responses", "Enabling /v1/responses endpoint", async () => {
|
|
@@ -2539,7 +2798,7 @@ export class InstallerStepEngine {
|
|
|
2539
2798
|
});
|
|
2540
2799
|
|
|
2541
2800
|
await this.runStep("gateway_scheduling", "Configuring heartbeat and cron schedules", async () => {
|
|
2542
|
-
const result = configureGatewayScheduling(this.modeConfig, CONFIG_FILE);
|
|
2801
|
+
const result = configureGatewayScheduling(this.modeConfig, CONFIG_FILE, this.options);
|
|
2543
2802
|
this.emitLog("gateway_scheduling", "info", `Agents configured: ${result.agentsConfigured}`);
|
|
2544
2803
|
if (result.cronJobsStoreWriteOk) {
|
|
2545
2804
|
this.emitLog(
|
|
@@ -2560,17 +2819,6 @@ export class InstallerStepEngine {
|
|
|
2560
2819
|
this.emitLog("gateway_scheduling", "warn", "Removed legacy 'cron.jobs' from openclaw.json to keep config validation compatible.");
|
|
2561
2820
|
}
|
|
2562
2821
|
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
|
-
}
|
|
2574
2822
|
const restart = await restartGateway();
|
|
2575
2823
|
return { ...result, restart };
|
|
2576
2824
|
});
|
|
@@ -2628,23 +2876,71 @@ export class InstallerStepEngine {
|
|
|
2628
2876
|
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.`);
|
|
2629
2877
|
}
|
|
2630
2878
|
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
|
+
|
|
2631
2905
|
const verified = [];
|
|
2632
2906
|
const identitiesToPersist = [];
|
|
2633
|
-
for (const
|
|
2634
|
-
const
|
|
2635
|
-
|
|
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++) {
|
|
2636
2912
|
try {
|
|
2637
|
-
|
|
2638
|
-
if (check.ok)
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
this.emitLog("x_credentials", "warn", `X credential verification failed for '${agentId}': HTTP ${check.status}`);
|
|
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));
|
|
2644
2919
|
}
|
|
2645
2920
|
} catch (err) {
|
|
2646
|
-
|
|
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 });
|
|
2647
2937
|
}
|
|
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
|
+
);
|
|
2648
2944
|
}
|
|
2649
2945
|
}
|
|
2650
2946
|
if (identitiesToPersist.length > 0) {
|
|
@@ -2663,6 +2959,25 @@ export class InstallerStepEngine {
|
|
|
2663
2959
|
return { checks };
|
|
2664
2960
|
});
|
|
2665
2961
|
|
|
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
|
+
|
|
2666
2981
|
this.state.status = "completed";
|
|
2667
2982
|
this.state.completedAt = nowIso();
|
|
2668
2983
|
return this.state;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "traderclaw-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.95-beta.0",
|
|
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.95-beta.0"
|
|
21
21
|
},
|
|
22
22
|
"keywords": [
|
|
23
23
|
"traderclaw",
|