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 +36 -5
- package/bin/installer-step-engine.mjs +598 -0
- package/bin/openclaw-trader.mjs +526 -4
- package/dist/index.js +282 -3
- package/openclaw.plugin.json +18 -0
- package/package.json +1 -1
- package/skills/solana-trader/SKILL.md +527 -62
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
|
|
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
|
|
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
|
|
173
|
+
openclaw gateway restart
|
|
143
174
|
```
|
|
144
175
|
|
|
145
|
-
## Available Tools (
|
|
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
|
+
}
|