traderclaw-v1 1.0.7 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,7 +7,7 @@ TraderClaw V1 plugin for autonomous Solana memecoin trading. Connects OpenClaw t
7
7
  ```
8
8
  OpenClaw Agent (brain: reasoning, decisions, strategy evolution)
9
9
 
10
- │ calls 26 typed tools
10
+ │ calls 52 typed tools
11
11
 
12
12
  Plugin (this package) ── HTTP ──→ Orchestrator (data + risk + execution)
13
13
  │ │
@@ -51,7 +51,38 @@ The setup wizard will:
51
51
  That's it. Restart the gateway and start trading:
52
52
 
53
53
  ```bash
54
- openclaw gateway --restart
54
+ openclaw gateway restart
55
+ ```
56
+
57
+ ### Optional: Simple localhost installer wizard (Linux-first)
58
+
59
+ ```bash
60
+ traderclaw install --wizard
61
+ ```
62
+
63
+ This opens a localhost UI that runs prechecks, lane-aware setup, gateway validation, optional Telegram setup, and final verification.
64
+
65
+ ### Optional: Run CLI prechecks directly
66
+
67
+ ```bash
68
+ traderclaw precheck --dry-run --output linux-qa-dryrun.log
69
+ traderclaw precheck --allow-install --output linux-qa-install.log
70
+ ```
71
+
72
+ Use `--dry-run` for non-mutating validation and `--allow-install` for guided dependency installs.
73
+
74
+ ### 3. Run the mandatory startup sequence
75
+
76
+ Send this prompt to your bot after startup:
77
+
78
+ ```text
79
+ Run mandatory startup sequence and report pass/fail for each:
80
+ 1) solana_system_status
81
+ 2) solana_gateway_credentials_get (set if missing)
82
+ 3) solana_alpha_subscribe(agentId: "main")
83
+ 4) solana_capital_status
84
+ 5) solana_positions
85
+ 6) solana_killswitch_status
55
86
  ```
56
87
 
57
88
  ### Non-interactive setup
@@ -104,7 +135,7 @@ traderclaw config set <key> <v> # Update a value
104
135
  traderclaw config reset # Remove all plugin config
105
136
  ```
106
137
 
107
- Available config keys: `orchestratorUrl`, `walletId`, `apiKey`, `apiTimeout`
138
+ Available config keys: `orchestratorUrl`, `walletId`, `apiKey`, `apiTimeout`, `refreshToken`, `walletPublicKey`, `walletPrivateKey`, `gatewayBaseUrl`, `gatewayToken`, `agentId`
108
139
 
109
140
  ### `traderclaw --help`
110
141
 
@@ -139,10 +170,10 @@ If you prefer to configure manually instead of using the CLI, add to `~/.opencla
139
170
  Restart the gateway after configuration:
140
171
 
141
172
  ```bash
142
- openclaw gateway --restart
173
+ openclaw gateway restart
143
174
  ```
144
175
 
145
- ## Available Tools (26)
176
+ ## Available Tools (52)
146
177
 
147
178
  ### Scanning
148
179
  | Tool | Description |
@@ -0,0 +1,598 @@
1
+ import { execSync, spawn } from "child_process";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3
+ import { homedir } from "os";
4
+ import { join } from "path";
5
+
6
+ const CONFIG_DIR = join(homedir(), ".openclaw");
7
+ const CONFIG_FILE = join(CONFIG_DIR, "openclaw.json");
8
+
9
+ function commandExists(cmd) {
10
+ try {
11
+ execSync(`command -v ${cmd}`, { stdio: "ignore", shell: true });
12
+ return true;
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
17
+
18
+ function getCommandOutput(cmd) {
19
+ try {
20
+ return execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], shell: true }).trim();
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+
26
+ function extractUrls(text = "") {
27
+ const matches = text.match(/https?:\/\/[^\s"')]+/g);
28
+ return matches ? [...new Set(matches)] : [];
29
+ }
30
+
31
+ function shellQuote(value) {
32
+ const raw = String(value ?? "");
33
+ if (raw.length === 0) return "''";
34
+ return `'${raw.replace(/'/g, `'\\''`)}'`;
35
+ }
36
+
37
+ function buildCommandString(cmd, args = []) {
38
+ return [cmd, ...args].map((part) => shellQuote(part)).join(" ");
39
+ }
40
+
41
+ function isPrivilegeError(err) {
42
+ const text = `${err?.message || ""}\n${err?.stderr || ""}\n${err?.stdout || ""}`.toLowerCase();
43
+ return (
44
+ text.includes("permission denied")
45
+ || text.includes("eacces")
46
+ || text.includes("access denied")
47
+ || text.includes("operation not permitted")
48
+ || text.includes("must be root")
49
+ || text.includes("requires root")
50
+ || text.includes("sudo")
51
+ || text.includes("authentication is required")
52
+ );
53
+ }
54
+
55
+ function isRootUser() {
56
+ return typeof process.getuid === "function" && process.getuid() === 0;
57
+ }
58
+
59
+ function canUseSudoWithoutPrompt() {
60
+ try {
61
+ execSync("sudo -n true", { stdio: "ignore", shell: true });
62
+ return true;
63
+ } catch {
64
+ return false;
65
+ }
66
+ }
67
+
68
+ function tailscalePermissionRemediation() {
69
+ return [
70
+ "Tailscale requires elevated permissions on this host.",
71
+ "Run these commands in your terminal, then click Start Installation again:",
72
+ "1) sudo tailscale set --operator=$USER",
73
+ "2) sudo tailscale up",
74
+ "3) tailscale status",
75
+ ].join("\n");
76
+ }
77
+
78
+ function privilegeRemediationMessage(cmd, args = [], customLines = []) {
79
+ const command = buildCommandString(cmd, args);
80
+ const lines = [
81
+ "This step needs elevated privileges on this host.",
82
+ "Run this command in your terminal, then click Start Installation again:",
83
+ `sudo ${command}`,
84
+ ];
85
+ if (customLines.length > 0) {
86
+ lines.push(...customLines);
87
+ }
88
+ return lines.join("\n");
89
+ }
90
+
91
+ function gatewayTimeoutRemediation() {
92
+ return [
93
+ "Gateway bootstrap timed out waiting for health checks.",
94
+ "Run these commands in terminal, then click Start Installation again:",
95
+ "1) openclaw gateway status --json || true",
96
+ "2) openclaw gateway probe || true",
97
+ "3) openclaw gateway stop || true",
98
+ "4) openclaw gateway install",
99
+ "5) openclaw gateway restart",
100
+ "6) openclaw gateway status --json",
101
+ "7) tailscale funnel --bg 18789",
102
+ "8) tailscale funnel status",
103
+ "If gateway still fails on a low-memory VM, add swap or use a larger staging size (>=2GB RAM recommended).",
104
+ ].join("\n");
105
+ }
106
+
107
+ function gatewayModeUnsetRemediation() {
108
+ return [
109
+ "Gateway start is blocked because gateway.mode is unset.",
110
+ "Run these commands in terminal, then click Start Installation again:",
111
+ "1) cp ~/.openclaw/openclaw.json ~/.openclaw/openclaw.json.bak.$(date +%s) || true",
112
+ "2) openclaw config set gateway.mode local",
113
+ "3) openclaw config set gateway.bind loopback",
114
+ "4) openclaw gateway restart",
115
+ "5) openclaw gateway status --json",
116
+ ].join("\n");
117
+ }
118
+
119
+ function runCommandWithEvents(cmd, args = [], opts = {}) {
120
+ return new Promise((resolve, reject) => {
121
+ const child = spawn(cmd, args, {
122
+ stdio: "pipe",
123
+ shell: true,
124
+ ...opts,
125
+ });
126
+
127
+ let stdout = "";
128
+ let stderr = "";
129
+ const onEvent = typeof opts.onEvent === "function" ? opts.onEvent : null;
130
+ const emit = (event) => onEvent && onEvent(event);
131
+
132
+ child.stdout?.on("data", (d) => {
133
+ const text = d.toString();
134
+ stdout += text;
135
+ emit({ type: "stdout", text, urls: extractUrls(text) });
136
+ });
137
+
138
+ child.stderr?.on("data", (d) => {
139
+ const text = d.toString();
140
+ stderr += text;
141
+ emit({ type: "stderr", text, urls: extractUrls(text) });
142
+ });
143
+
144
+ child.on("close", (code) => {
145
+ const urls = [...new Set([...extractUrls(stdout), ...extractUrls(stderr)])];
146
+ if (code === 0) resolve({ stdout, stderr, code, urls });
147
+ else {
148
+ const stderrPreview = (stderr || "").trim().split("\n").slice(-6).join("\n");
149
+ const err = new Error(stderrPreview ? `command failed with exit code ${code}: ${stderrPreview}` : `command failed with exit code ${code}`);
150
+ err.code = code;
151
+ err.stdout = stdout;
152
+ err.stderr = stderr;
153
+ err.urls = urls;
154
+ reject(err);
155
+ }
156
+ });
157
+ child.on("error", reject);
158
+ });
159
+ }
160
+
161
+ async function installOpenClawPlatform() {
162
+ if (commandExists("openclaw")) {
163
+ return { alreadyInstalled: true, version: getCommandOutput("openclaw --version") };
164
+ }
165
+ await runCommandWithEvents("npm", ["install", "-g", "openclaw"]);
166
+ return { alreadyInstalled: false, installed: true, available: commandExists("openclaw") };
167
+ }
168
+
169
+ async function installPlugin(modeConfig) {
170
+ await runCommandWithEvents("npm", ["install", "-g", modeConfig.pluginPackage]);
171
+ return { installed: true, available: commandExists(modeConfig.cliName) };
172
+ }
173
+
174
+ function ensureOpenResponsesEnabled(configPath = CONFIG_FILE) {
175
+ let config = {};
176
+ try {
177
+ config = JSON.parse(readFileSync(configPath, "utf-8"));
178
+ } catch {
179
+ config = {};
180
+ }
181
+
182
+ if (!config.gateway) config.gateway = {};
183
+ if (!config.gateway.http) config.gateway.http = {};
184
+ if (!config.gateway.http.endpoints) config.gateway.http.endpoints = {};
185
+ if (!config.gateway.http.endpoints.responses) config.gateway.http.endpoints.responses = {};
186
+ config.gateway.http.endpoints.responses.enabled = true;
187
+
188
+ mkdirSync(CONFIG_DIR, { recursive: true });
189
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
190
+ return configPath;
191
+ }
192
+
193
+ async function restartGateway() {
194
+ if (!commandExists("openclaw")) return { ran: false };
195
+ try {
196
+ await runCommandWithEvents("openclaw", ["gateway", "restart"]);
197
+ return { ran: true, success: true };
198
+ } catch {
199
+ return { ran: true, success: false };
200
+ }
201
+ }
202
+
203
+ function deployGatewayConfig(modeConfig) {
204
+ const gatewayDir = join(CONFIG_DIR, "gateway");
205
+ mkdirSync(gatewayDir, { recursive: true });
206
+ const destFile = join(gatewayDir, modeConfig.gatewayConfig);
207
+ const npmRoot = getCommandOutput("npm root -g");
208
+ if (!npmRoot) return { deployed: false, dest: destFile };
209
+ const src = join(npmRoot, modeConfig.pluginPackage, "config", modeConfig.gatewayConfig);
210
+ if (!existsSync(src)) return { deployed: false, dest: destFile };
211
+ writeFileSync(destFile, readFileSync(src));
212
+ return { deployed: true, source: src, dest: destFile };
213
+ }
214
+
215
+ function verifyInstallation(modeConfig, apiKey) {
216
+ const gatewayFile = join(CONFIG_DIR, "gateway", modeConfig.gatewayConfig);
217
+ return [
218
+ { label: "OpenClaw platform", ok: commandExists("openclaw"), note: "not in PATH" },
219
+ { label: `Trading CLI (${modeConfig.cliName})`, ok: commandExists(modeConfig.cliName), note: "not in PATH" },
220
+ { label: "Configuration file", ok: existsSync(CONFIG_FILE), note: "not created" },
221
+ { label: "Gateway configuration", ok: existsSync(gatewayFile), note: "not found" },
222
+ { label: "API key configured", ok: !!apiKey, note: "needs setup" },
223
+ ];
224
+ }
225
+
226
+ function nowIso() {
227
+ return new Date().toISOString();
228
+ }
229
+
230
+ const URL_REGEX = /https?:\/\/[^\s"')]+/g;
231
+ function firstUrl(text = "") {
232
+ const found = text.match(URL_REGEX);
233
+ return found?.[0] || null;
234
+ }
235
+
236
+ function normalizeLane(input) {
237
+ return input === "event-driven" ? "event-driven" : "quick-local";
238
+ }
239
+
240
+ export class InstallerStepEngine {
241
+ constructor(modeConfig, options = {}, hooks = {}) {
242
+ this.modeConfig = modeConfig;
243
+ this.options = {
244
+ lane: normalizeLane(options.lane),
245
+ apiKey: options.apiKey || "",
246
+ orchestratorUrl: options.orchestratorUrl || "https://api.traderclaw.ai",
247
+ gatewayBaseUrl: options.gatewayBaseUrl || "",
248
+ gatewayToken: options.gatewayToken || "",
249
+ enableTelegram: options.enableTelegram === true,
250
+ telegramToken: options.telegramToken || "",
251
+ autoInstallDeps: options.autoInstallDeps !== false,
252
+ skipPreflight: options.skipPreflight === true,
253
+ skipInstallOpenClaw: options.skipInstallOpenClaw === true,
254
+ skipInstallPlugin: options.skipInstallPlugin === true,
255
+ skipTailscale: options.skipTailscale === true,
256
+ skipGatewayBootstrap: options.skipGatewayBootstrap === true,
257
+ skipGatewayConfig: options.skipGatewayConfig === true,
258
+ };
259
+ this.hooks = {
260
+ onStepEvent: typeof hooks.onStepEvent === "function" ? hooks.onStepEvent : () => {},
261
+ onLog: typeof hooks.onLog === "function" ? hooks.onLog : () => {},
262
+ };
263
+ this.state = {
264
+ startedAt: null,
265
+ completedAt: null,
266
+ status: "idle",
267
+ errors: [],
268
+ detected: { funnelUrl: null, tailscaleApprovalUrl: null },
269
+ stepResults: [],
270
+ verifyChecks: [],
271
+ setupHandoff: null,
272
+ autoRecovery: {
273
+ gatewayModeRecoveryAttempted: false,
274
+ gatewayModeRecoverySucceeded: false,
275
+ backupPath: null,
276
+ },
277
+ };
278
+ }
279
+
280
+ async runWithPrivilegeGuidance(stepId, cmd, args = [], customLines = []) {
281
+ try {
282
+ return await runCommandWithEvents(cmd, args, {
283
+ onEvent: (evt) => this.emitLog(stepId, evt.type === "stderr" ? "warn" : "info", evt.text, evt.urls || []),
284
+ });
285
+ } catch (err) {
286
+ if (isPrivilegeError(err)) {
287
+ throw new Error(privilegeRemediationMessage(cmd, args, customLines));
288
+ }
289
+ throw err;
290
+ }
291
+ }
292
+
293
+ emitStep(stepId, status, detail = "") {
294
+ this.hooks.onStepEvent({ at: nowIso(), stepId, status, detail });
295
+ }
296
+
297
+ emitLog(stepId, level, text, urls = []) {
298
+ this.hooks.onLog({ at: nowIso(), stepId, level, text, urls });
299
+ }
300
+
301
+ async runStep(stepId, title, handler) {
302
+ this.emitStep(stepId, "in_progress", title);
303
+ const startedAt = nowIso();
304
+ try {
305
+ const result = await handler();
306
+ this.state.stepResults.push({ stepId, title, status: "completed", startedAt, completedAt: nowIso(), result });
307
+ this.emitStep(stepId, "completed", title);
308
+ return result;
309
+ } catch (err) {
310
+ const detail = err?.message || String(err);
311
+ this.state.stepResults.push({ stepId, title, status: "failed", startedAt, completedAt: nowIso(), error: detail });
312
+ this.state.errors.push({ stepId, error: detail });
313
+ this.emitStep(stepId, "failed", detail);
314
+ throw err;
315
+ }
316
+ }
317
+
318
+ async ensureTailscale() {
319
+ if (commandExists("tailscale")) return { installed: true, alreadyInstalled: true };
320
+ if (!this.options.autoInstallDeps) throw new Error("tailscale missing and auto-install disabled");
321
+
322
+ if (!isRootUser() && !canUseSudoWithoutPrompt()) {
323
+ throw new Error(
324
+ [
325
+ "Tailscale is not installed and the installer cannot elevate privileges automatically.",
326
+ "Run this command in your terminal, then click Start Installation again:",
327
+ "sudo bash -lc 'curl -fsSL https://tailscale.com/install.sh | sh'",
328
+ ].join("\n"),
329
+ );
330
+ }
331
+
332
+ try {
333
+ if (isRootUser()) {
334
+ await this.runWithPrivilegeGuidance("tailscale", "bash", ["-lc", "curl -fsSL https://tailscale.com/install.sh | sh"]);
335
+ } else {
336
+ await this.runWithPrivilegeGuidance("tailscale", "sudo", ["bash", "-lc", "curl -fsSL https://tailscale.com/install.sh | sh"]);
337
+ }
338
+ } catch (err) {
339
+ const message = `${err?.message || ""} ${err?.stderr || ""}`.toLowerCase();
340
+ if (message.includes("sudo") || message.includes("password")) {
341
+ throw new Error(
342
+ [
343
+ "Tailscale installation requires terminal sudo approval.",
344
+ "Run this command in your terminal, then click Start Installation again:",
345
+ "sudo bash -lc 'curl -fsSL https://tailscale.com/install.sh | sh'",
346
+ ].join("\n"),
347
+ );
348
+ }
349
+ throw err;
350
+ }
351
+
352
+ return { installed: true, alreadyInstalled: false };
353
+ }
354
+
355
+ async runTailscaleUp() {
356
+ try {
357
+ const result = await runCommandWithEvents("tailscale", ["up"], {
358
+ onEvent: (evt) => {
359
+ const url = firstUrl(evt.text);
360
+ if (url && !this.state.detected.tailscaleApprovalUrl) this.state.detected.tailscaleApprovalUrl = url;
361
+ this.emitLog("tailscale_up", evt.type === "stderr" ? "warn" : "info", evt.text, evt.urls || []);
362
+ },
363
+ });
364
+ return { ok: true, approvalUrl: this.state.detected.tailscaleApprovalUrl, urls: result.urls || [] };
365
+ } catch (err) {
366
+ const details = `${err?.stderr || ""}\n${err?.stdout || ""}\n${err?.message || ""}`.toLowerCase();
367
+ if (
368
+ details.includes("access denied")
369
+ || details.includes("checkprefs")
370
+ || details.includes("prefs write access denied")
371
+ ) {
372
+ throw new Error(tailscalePermissionRemediation());
373
+ }
374
+ throw err;
375
+ }
376
+ }
377
+
378
+ async runFunnel() {
379
+ try {
380
+ await this.runWithPrivilegeGuidance("funnel", "tailscale", ["funnel", "--bg", "18789"]);
381
+ } catch (err) {
382
+ const details = `${err?.stderr || ""}\n${err?.stdout || ""}\n${err?.message || ""}`.toLowerCase();
383
+ if (details.includes("access denied") || details.includes("operator")) {
384
+ throw new Error(tailscalePermissionRemediation());
385
+ }
386
+ throw err;
387
+ }
388
+ const statusOut = getCommandOutput("tailscale funnel status") || "";
389
+ const funnelUrl = firstUrl(statusOut);
390
+ if (funnelUrl) this.state.detected.funnelUrl = funnelUrl;
391
+ this.emitLog("funnel", "info", statusOut);
392
+ return { funnelUrl };
393
+ }
394
+
395
+ readGatewayStatusSnapshot() {
396
+ const raw = getCommandOutput("openclaw gateway status --json || true");
397
+ if (!raw) return null;
398
+ try {
399
+ return JSON.parse(raw);
400
+ } catch {
401
+ return null;
402
+ }
403
+ }
404
+
405
+ isGatewayHealthy(statusJson) {
406
+ if (!statusJson || typeof statusJson !== "object") return false;
407
+ const serviceStatus = statusJson?.service?.runtime?.status;
408
+ const rpcOk = statusJson?.rpc?.ok === true;
409
+ return serviceStatus === "running" && rpcOk;
410
+ }
411
+
412
+ async tryAutoRecoverGatewayMode(stepId) {
413
+ if (this.state.autoRecovery.gatewayModeRecoveryAttempted) {
414
+ return { attempted: true, success: false, reason: "already_attempted" };
415
+ }
416
+ this.state.autoRecovery.gatewayModeRecoveryAttempted = true;
417
+
418
+ let config = {};
419
+ let rawOriginal = "{}\n";
420
+ try {
421
+ rawOriginal = readFileSync(CONFIG_FILE, "utf-8");
422
+ config = JSON.parse(rawOriginal);
423
+ } catch {
424
+ config = {};
425
+ }
426
+
427
+ if (!config.gateway) config.gateway = {};
428
+ const changed = [];
429
+ if (!config.gateway.mode) {
430
+ config.gateway.mode = "local";
431
+ changed.push("gateway.mode=local");
432
+ }
433
+ if (!config.gateway.bind) {
434
+ config.gateway.bind = "loopback";
435
+ changed.push("gateway.bind=loopback");
436
+ }
437
+ if (!Number.isInteger(config.gateway.port)) {
438
+ config.gateway.port = 18789;
439
+ changed.push("gateway.port=18789");
440
+ }
441
+
442
+ if (changed.length === 0) {
443
+ return { attempted: true, success: false, reason: "no_missing_gateway_defaults" };
444
+ }
445
+
446
+ mkdirSync(CONFIG_DIR, { recursive: true });
447
+ const backupPath = `${CONFIG_FILE}.bak.${Date.now()}`;
448
+ writeFileSync(backupPath, rawOriginal, "utf-8");
449
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", "utf-8");
450
+ this.state.autoRecovery.backupPath = backupPath;
451
+ this.emitLog(stepId, "warn", `Auto-recovery: applied ${changed.join(", ")} with backup at ${backupPath}`);
452
+
453
+ try {
454
+ await this.runWithPrivilegeGuidance(stepId, "openclaw", ["gateway", "stop"]);
455
+ } catch {
456
+ // best effort stop
457
+ }
458
+ await this.runWithPrivilegeGuidance(stepId, "openclaw", ["gateway", "install"]);
459
+ await this.runWithPrivilegeGuidance(stepId, "openclaw", ["gateway", "restart"]);
460
+
461
+ const status = this.readGatewayStatusSnapshot();
462
+ const healthy = this.isGatewayHealthy(status);
463
+ if (healthy) {
464
+ this.state.autoRecovery.gatewayModeRecoverySucceeded = true;
465
+ this.emitLog(stepId, "info", "Auto-recovery succeeded: gateway is healthy after restart.");
466
+ return { attempted: true, success: true, backupPath };
467
+ }
468
+ return { attempted: true, success: false, backupPath, reason: "gateway_not_healthy_after_recovery" };
469
+ }
470
+
471
+ async runTelegramStep() {
472
+ if (!this.options.enableTelegram) return { skipped: true, reason: "telegram_not_requested" };
473
+ if (!this.options.telegramToken) return { skipped: true, reason: "telegram_token_missing" };
474
+ await runCommandWithEvents("openclaw", ["plugins", "enable", "telegram"]);
475
+ await runCommandWithEvents("openclaw", ["channels", "add", "--channel", "telegram", "--token", this.options.telegramToken]);
476
+ await runCommandWithEvents("openclaw", ["channels", "status", "--probe"]);
477
+ return { configured: true };
478
+ }
479
+
480
+ buildSetupHandoff() {
481
+ const args = ["setup", "--url", this.options.orchestratorUrl || "https://api.traderclaw.ai"];
482
+ if (this.options.lane !== "event-driven") {
483
+ args.push("--skip-gateway-registration");
484
+ }
485
+ const gatewayBaseUrl = this.options.gatewayBaseUrl || this.state.detected.funnelUrl || "";
486
+ if (this.options.lane === "event-driven" && gatewayBaseUrl) {
487
+ args.push("--gateway-base-url", gatewayBaseUrl);
488
+ }
489
+
490
+ const command = [this.modeConfig.cliName, ...args].join(" ");
491
+ return {
492
+ pending: true,
493
+ command,
494
+ title: "Ready to launch your agentic trading desk",
495
+ message:
496
+ "Core install is complete. Final setup is intentionally handed off to your VPS shell so sensitive wallet prompts stay private.",
497
+ hint:
498
+ "Run the command in terminal, answer setup prompts, then restart gateway.",
499
+ restartCommand: "openclaw gateway restart",
500
+ };
501
+ }
502
+
503
+ async runAll() {
504
+ this.state.status = "running";
505
+ this.state.startedAt = nowIso();
506
+ try {
507
+ if (!this.options.skipPreflight) {
508
+ await this.runStep("preflight", "Checking prerequisites", async () => {
509
+ if (!commandExists("node") || !commandExists("npm")) throw new Error("node and npm are required");
510
+ return { node: true, npm: true, openclaw: commandExists("openclaw"), tailscale: commandExists("tailscale") };
511
+ });
512
+ }
513
+
514
+ if (!this.options.skipInstallOpenClaw) {
515
+ await this.runStep("install_openclaw", "Installing OpenClaw platform", async () => installOpenClawPlatform());
516
+ }
517
+ if (!this.options.skipInstallPlugin) {
518
+ await this.runStep("install_plugin", "Installing TraderClaw plugin package", async () => installPlugin(this.modeConfig));
519
+ }
520
+ if (!this.options.skipTailscale) {
521
+ await this.runStep("tailscale_install", "Ensuring Tailscale is installed", async () => this.ensureTailscale());
522
+ await this.runStep("tailscale_up", "Connecting Tailscale", async () => this.runTailscaleUp());
523
+ }
524
+ if (!this.options.skipGatewayBootstrap) {
525
+ await this.runStep("gateway_bootstrap", "Starting OpenClaw gateway and Funnel", async () => {
526
+ try {
527
+ await this.runWithPrivilegeGuidance("gateway_bootstrap", "openclaw", ["gateway", "install"]);
528
+ await this.runWithPrivilegeGuidance("gateway_bootstrap", "openclaw", ["gateway", "restart"]);
529
+ return this.runFunnel();
530
+ } catch (err) {
531
+ const text = `${err?.message || ""}\n${err?.stderr || ""}\n${err?.stdout || ""}`.toLowerCase();
532
+ const gatewayModeUnset = text.includes("gateway.mode=local") && text.includes("current: unset");
533
+ if (
534
+ text.includes("gateway restart timed out")
535
+ || text.includes("timed out after 60s waiting for health checks")
536
+ || text.includes("waiting for gateway port")
537
+ || gatewayModeUnset
538
+ ) {
539
+ const recovered = await this.tryAutoRecoverGatewayMode("gateway_bootstrap");
540
+ if (recovered.success) {
541
+ return this.runFunnel();
542
+ }
543
+ if (gatewayModeUnset) {
544
+ throw new Error(gatewayModeUnsetRemediation());
545
+ }
546
+ throw new Error(gatewayTimeoutRemediation());
547
+ }
548
+ throw err;
549
+ }
550
+ });
551
+ }
552
+
553
+ await this.runStep("enable_responses", "Enabling /v1/responses endpoint", async () => {
554
+ const configPath = ensureOpenResponsesEnabled(CONFIG_FILE);
555
+ const restart = await restartGateway();
556
+ return { configPath, restart };
557
+ });
558
+
559
+ await this.runStep("setup_handoff", "Preparing secure setup handoff", async () => {
560
+ const handoff = this.buildSetupHandoff();
561
+ this.state.setupHandoff = handoff;
562
+ this.emitLog("setup_handoff", "info", handoff.title);
563
+ this.emitLog("setup_handoff", "info", handoff.message);
564
+ this.emitLog("setup_handoff", "info", `Run in VPS shell: ${handoff.command}`);
565
+ this.emitLog("setup_handoff", "info", `Then run: ${handoff.restartCommand}`);
566
+ return handoff;
567
+ });
568
+
569
+ if (!this.options.skipGatewayConfig) {
570
+ await this.runStep("gateway_config", "Deploying gateway config and restarting", async () => {
571
+ const deploy = deployGatewayConfig(this.modeConfig);
572
+ const restart = await restartGateway();
573
+ return { deploy, restart };
574
+ });
575
+ }
576
+
577
+ await this.runStep("telegram_optional", "Optional Telegram setup", async () => this.runTelegramStep());
578
+ await this.runStep("verify", "Verifying installation", async () => {
579
+ const checks = verifyInstallation(this.modeConfig, this.options.apiKey);
580
+ this.state.verifyChecks = checks;
581
+ return { checks };
582
+ });
583
+
584
+ this.state.status = "completed";
585
+ this.state.completedAt = nowIso();
586
+ return this.state;
587
+ } catch (err) {
588
+ this.state.status = "failed";
589
+ this.state.completedAt = nowIso();
590
+ this.state.errors.push({ stepId: "runtime", error: err?.message || String(err) });
591
+ return this.state;
592
+ }
593
+ }
594
+ }
595
+
596
+ export function createInstallerStepEngine(modeConfig, options = {}, hooks = {}) {
597
+ return new InstallerStepEngine(modeConfig, options, hooks);
598
+ }