traderclaw-cli 1.0.51

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.
@@ -0,0 +1,3148 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createInterface } from "readline";
4
+ import { readFileSync, writeFileSync, mkdirSync, appendFileSync, existsSync } from "fs";
5
+ import { join } from "path";
6
+ import { homedir } from "os";
7
+ import { randomUUID, createPrivateKey, sign as cryptoSign } from "crypto";
8
+ import { execSync } from "child_process";
9
+ import { createServer } from "http";
10
+ import { sortModelsByPreference } from "./llm-model-preference.mjs";
11
+ import { resolvePluginPackageRoot } from "./resolve-plugin-root.mjs";
12
+
13
+ const PLUGIN_ROOT = resolvePluginPackageRoot(import.meta.url);
14
+ const PACKAGE_JSON = JSON.parse(readFileSync(join(PLUGIN_ROOT, "package.json"), "utf-8"));
15
+ const VERSION = PACKAGE_JSON.version;
16
+ const NPM_PACKAGE_NAME = typeof PACKAGE_JSON.name === "string" ? PACKAGE_JSON.name : "solana-traderclaw";
17
+ const PLUGIN_ID = "solana-trader";
18
+ const LEGACY_PLUGIN_IDS = ["traderclaw-v1", "solana-traderclaw-v1", "solana-traderclaw"];
19
+ const CONFIG_DIR = join(homedir(), ".openclaw");
20
+ const CONFIG_FILE = join(CONFIG_DIR, "openclaw.json");
21
+ const WALLET_PRIVATE_KEY_ENV = "TRADERCLAW_WALLET_PRIVATE_KEY";
22
+
23
+ /** Linked from CLI errors and setup — keep in sync with SKILL / README. */
24
+ const TRADERCLAW_SESSION_TROUBLESHOOTING_URL =
25
+ "https://docs.traderclaw.ai/docs/installation#troubleshooting-session-expired-auth-errors-or-the-agent-logged-out";
26
+
27
+ function printSessionTroubleshootingHint() {
28
+ printWarn(` Troubleshooting: ${TRADERCLAW_SESSION_TROUBLESHOOTING_URL}`);
29
+ printWarn(
30
+ " Wallet proof is not signup — it proves you own the trading wallet already linked to your API key.",
31
+ );
32
+ printWarn(
33
+ " The OpenClaw gateway is a separate process: export in SSH alone does not set the key for the gateway. Use systemd EnvironmentFile (or equivalent) so TRADERCLAW_WALLET_PRIVATE_KEY is available to the gateway service.",
34
+ );
35
+ }
36
+
37
+ const BANNER = `
38
+ ████████╗██████╗ █████╗ ██████╗ ███████╗██████╗ ██████╗██╗ █████╗ ██╗ ██╗
39
+ ╚══██╔══╝██╔══██╗██╔══██╗██╔══██╗██╔════╝██╔══██╗██╔════╝██║ ██╔══██╗██║ ██║
40
+ ██║ ██████╔╝███████║██║ ██║█████╗ ██████╔╝██║ ██║ ███████║██║ █╗ ██║
41
+ ██║ ██╔══██╗██╔══██║██║ ██║██╔══╝ ██╔══██╗██║ ██║ ██╔══██║██║███╗██║
42
+ ██║ ██║ ██║██║ ██║██████╔╝███████╗██║ ██║╚██████╗███████╗██║ ██║╚███╔███╔╝
43
+ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝
44
+ Solana Memecoin Trading Agent (V1)
45
+ `;
46
+
47
+ function print(msg) {
48
+ process.stdout.write(msg + "\n");
49
+ }
50
+
51
+ function printError(msg) {
52
+ process.stderr.write(`\x1b[31mError: ${msg}\x1b[0m\n`);
53
+ }
54
+
55
+ function printSuccess(msg) {
56
+ print(`\x1b[32m${msg}\x1b[0m`);
57
+ }
58
+
59
+ function printWarn(msg) {
60
+ print(`\x1b[33m${msg}\x1b[0m`);
61
+ }
62
+
63
+ function printInfo(msg) {
64
+ print(`\x1b[36m${msg}\x1b[0m`);
65
+ }
66
+
67
+ function commandExists(cmd) {
68
+ try {
69
+ execSync(`command -v ${cmd}`, { stdio: "ignore", shell: true });
70
+ return true;
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+
76
+ function stripAnsi(text) {
77
+ if (typeof text !== "string") return text;
78
+ return text
79
+ .replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "")
80
+ .replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "")
81
+ .replace(/\x1b[^[\]]/g, "")
82
+ .replace(/\x1b/g, "");
83
+ }
84
+
85
+ /**
86
+ * Extract and parse the first valid JSON object or array from a string that may contain
87
+ * non-JSON prefix/suffix text (e.g. progress lines OpenClaw prints to stdout before the JSON).
88
+ * Returns the parsed value or null.
89
+ */
90
+ function extractJson(raw) {
91
+ if (typeof raw !== "string" || !raw.trim()) return null;
92
+ const cleaned = stripAnsi(raw);
93
+
94
+ // Fast path: whole string is valid JSON
95
+ try { return JSON.parse(cleaned); } catch {}
96
+
97
+ // Scan for the first `{` (object) or `[` (array) and try to parse from there.
98
+ // We try both start positions and pick the one that comes first in the string.
99
+ const objIdx = cleaned.indexOf("{");
100
+ const arrIdx = cleaned.indexOf("[");
101
+
102
+ const candidates = [];
103
+ if (objIdx >= 0) candidates.push(objIdx);
104
+ if (arrIdx >= 0) candidates.push(arrIdx);
105
+ candidates.sort((a, b) => a - b);
106
+
107
+ for (const start of candidates) {
108
+ const slice = cleaned.slice(start);
109
+ try { return JSON.parse(slice); } catch {}
110
+
111
+ // If the tail has trailing garbage, trim from the right matching bracket.
112
+ const endChar = cleaned[start] === "{" ? "}" : "]";
113
+ const end = cleaned.lastIndexOf(endChar);
114
+ if (end > start) {
115
+ try { return JSON.parse(cleaned.slice(start, end + 1)); } catch {}
116
+ }
117
+ }
118
+
119
+ return null;
120
+ }
121
+
122
+ /** Env vars passed to every openclaw CLI invocation to suppress colour output. */
123
+ const NO_COLOR_ENV = { ...process.env, NO_COLOR: "1", FORCE_COLOR: "0" };
124
+
125
+ function getCommandOutput(cmd) {
126
+ try {
127
+ return execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], shell: true, maxBuffer: 50 * 1024 * 1024, env: NO_COLOR_ENV }).trim();
128
+ } catch {
129
+ return null;
130
+ }
131
+ }
132
+
133
+ function maskKey(key) {
134
+ if (!key || key.length <= 8) return "****";
135
+ return key.slice(0, 4) + "..." + key.slice(-4);
136
+ }
137
+
138
+ function getRuntimeWalletPrivateKey(explicitValue = "") {
139
+ const fromArg = typeof explicitValue === "string" ? explicitValue.trim() : "";
140
+ if (fromArg) return fromArg;
141
+ const fromEnv = typeof process.env[WALLET_PRIVATE_KEY_ENV] === "string" ? process.env[WALLET_PRIVATE_KEY_ENV].trim() : "";
142
+ return fromEnv || "";
143
+ }
144
+
145
+ function removeLegacyWalletPrivateKey(pluginConfig) {
146
+ if (!pluginConfig || typeof pluginConfig !== "object") return false;
147
+ if (!Object.prototype.hasOwnProperty.call(pluginConfig, "walletPrivateKey")) return false;
148
+ delete pluginConfig.walletPrivateKey;
149
+ return true;
150
+ }
151
+
152
+ function prompt(question, defaultValue) {
153
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
154
+ const suffix = defaultValue ? ` (${defaultValue})` : "";
155
+ return new Promise((resolve) => {
156
+ rl.question(`${question}${suffix}: `, (answer) => {
157
+ rl.close();
158
+ resolve(answer.trim() || defaultValue || "");
159
+ });
160
+ });
161
+ }
162
+
163
+ async function confirm(question) {
164
+ const answer = await prompt(`${question} (y/n)`, "n");
165
+ return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
166
+ }
167
+
168
+ async function httpRequest(url, opts = {}) {
169
+ const controller = new AbortController();
170
+ const timeoutId = setTimeout(() => controller.abort(), opts.timeout ?? 10000);
171
+
172
+ try {
173
+ const headers = { "Content-Type": "application/json" };
174
+ if (opts.accessToken) {
175
+ headers["Authorization"] = `Bearer ${opts.accessToken}`;
176
+ } else if (opts.apiKey) {
177
+ headers["Authorization"] = `Bearer ${opts.apiKey}`;
178
+ }
179
+
180
+ const fetchOpts = {
181
+ method: opts.method || "GET",
182
+ headers,
183
+ signal: controller.signal,
184
+ };
185
+
186
+ if (opts.body) {
187
+ fetchOpts.body = JSON.stringify(opts.body);
188
+ }
189
+
190
+ const res = await fetch(url, fetchOpts);
191
+ const text = await res.text();
192
+
193
+ let data;
194
+ try {
195
+ data = JSON.parse(text);
196
+ } catch {
197
+ data = { raw: text };
198
+ }
199
+
200
+ return { ok: res.ok, status: res.status, data };
201
+ } catch (err) {
202
+ if (err && err.name === "AbortError") {
203
+ throw new Error(`Request timed out after ${opts.timeout ?? 10000}ms`);
204
+ }
205
+ throw err;
206
+ } finally {
207
+ clearTimeout(timeoutId);
208
+ }
209
+ }
210
+
211
+ function readConfig() {
212
+ try {
213
+ const raw = readFileSync(CONFIG_FILE, "utf-8");
214
+ return JSON.parse(raw);
215
+ } catch {
216
+ return {};
217
+ }
218
+ }
219
+
220
+ function writeConfig(config) {
221
+ normalizePluginConfigShape(config);
222
+ mkdirSync(CONFIG_DIR, { recursive: true });
223
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", "utf-8");
224
+ }
225
+
226
+ function isRecord(value) {
227
+ return !!value && typeof value === "object" && !Array.isArray(value);
228
+ }
229
+
230
+ function normalizePluginConfigShape(config) {
231
+ if (!isRecord(config)) return config;
232
+ if (!isRecord(config.plugins)) config.plugins = {};
233
+ const plugins = config.plugins;
234
+ if (!isRecord(plugins.entries)) plugins.entries = {};
235
+ const entries = plugins.entries;
236
+
237
+ let enabledSeen = false;
238
+ let enabledValue = false;
239
+ let mergedConfig = {};
240
+ let found = false;
241
+
242
+ for (const sourceId of [...LEGACY_PLUGIN_IDS, PLUGIN_ID]) {
243
+ const entry = entries[sourceId];
244
+ if (!isRecord(entry)) continue;
245
+ found = true;
246
+ if (typeof entry.enabled === "boolean") {
247
+ enabledSeen = true;
248
+ enabledValue = enabledValue || entry.enabled;
249
+ }
250
+ if (isRecord(entry.config)) {
251
+ mergedConfig = { ...mergedConfig, ...entry.config };
252
+ }
253
+ }
254
+
255
+ if (found) {
256
+ const canonical = isRecord(entries[PLUGIN_ID]) ? entries[PLUGIN_ID] : {};
257
+ entries[PLUGIN_ID] = {
258
+ ...canonical,
259
+ enabled: typeof canonical.enabled === "boolean" ? canonical.enabled : (enabledSeen ? enabledValue : true),
260
+ config: mergedConfig,
261
+ };
262
+ }
263
+
264
+ for (const legacyId of LEGACY_PLUGIN_IDS) {
265
+ delete entries[legacyId];
266
+ }
267
+
268
+ if (Array.isArray(plugins.allow)) {
269
+ const seen = new Set();
270
+ plugins.allow = plugins.allow.filter((id) => {
271
+ if (typeof id !== "string") return false;
272
+ const trimmed = id.trim();
273
+ if (!trimmed || LEGACY_PLUGIN_IDS.includes(trimmed) || seen.has(trimmed)) return false;
274
+ seen.add(trimmed);
275
+ return true;
276
+ });
277
+ }
278
+
279
+ return config;
280
+ }
281
+
282
+ function getPluginConfig(config) {
283
+ normalizePluginConfigShape(config);
284
+ const plugins = config.plugins;
285
+ if (!plugins) return null;
286
+ const entries = plugins.entries;
287
+ if (!entries) return null;
288
+ const plugin = entries[PLUGIN_ID];
289
+ if (!plugin) return null;
290
+ return plugin.config || null;
291
+ }
292
+
293
+ function setPluginConfig(config, pluginConfig) {
294
+ normalizePluginConfigShape(config);
295
+ if (!config.plugins) config.plugins = {};
296
+ if (!config.plugins.entries) config.plugins.entries = {};
297
+ config.plugins.entries[PLUGIN_ID] = {
298
+ enabled: true,
299
+ config: pluginConfig,
300
+ };
301
+ }
302
+
303
+ function getGatewayConfig(config) {
304
+ if (!config || typeof config !== "object") return {};
305
+ if (!config.gateway || typeof config.gateway !== "object") return {};
306
+ return config.gateway;
307
+ }
308
+
309
+ function detectTailscaleDnsName() {
310
+ try {
311
+ const raw = execSync("tailscale status --json", {
312
+ stdio: ["ignore", "pipe", "ignore"],
313
+ encoding: "utf8",
314
+ });
315
+ const parsed = JSON.parse(raw);
316
+ const dns = parsed?.Self?.DNSName;
317
+ if (typeof dns !== "string" || dns.length === 0) return undefined;
318
+ return dns.endsWith(".") ? dns.slice(0, -1) : dns;
319
+ } catch {
320
+ return undefined;
321
+ }
322
+ }
323
+
324
+ function buildGatewayDefaults(config) {
325
+ const gateway = getGatewayConfig(config);
326
+ const bind = typeof gateway.bind === "string" ? gateway.bind : "loopback";
327
+ const port = Number.isInteger(gateway.port) ? gateway.port : 18789;
328
+ const tailscaleMode = gateway?.tailscale && typeof gateway.tailscale === "object" && typeof gateway.tailscale.mode === "string"
329
+ ? gateway.tailscale.mode
330
+ : "off";
331
+ const gatewayToken = gateway?.auth && typeof gateway.auth === "object" && gateway.auth.mode === "token" && typeof gateway.auth.token === "string"
332
+ ? gateway.auth.token
333
+ : undefined;
334
+
335
+ let gatewayBaseUrl;
336
+ if (bind === "tailnet" || tailscaleMode === "serve" || tailscaleMode === "funnel") {
337
+ const dnsName = detectTailscaleDnsName();
338
+ if (dnsName) {
339
+ gatewayBaseUrl = `https://${dnsName}`;
340
+ }
341
+ } else if (bind === "lan" || bind === "custom") {
342
+ gatewayBaseUrl = `http://:${port}`;
343
+ }
344
+
345
+ return {
346
+ bind,
347
+ port,
348
+ tailscaleMode,
349
+ gatewayBaseUrl,
350
+ gatewayToken,
351
+ };
352
+ }
353
+
354
+ function getNestedBool(payload, key) {
355
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
356
+ if (typeof payload[key] === "boolean") return payload[key];
357
+ if (payload.data && typeof payload.data === "object" && !Array.isArray(payload.data) && typeof payload.data[key] === "boolean") {
358
+ return payload.data[key];
359
+ }
360
+ return undefined;
361
+ }
362
+
363
+ function extractWalletId(payload) {
364
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return null;
365
+ if (typeof payload.id === "string" || typeof payload.id === "number") return String(payload.id);
366
+ if (typeof payload.walletId === "string" || typeof payload.walletId === "number") return String(payload.walletId);
367
+ if (payload.wallet && typeof payload.wallet === "object" && !Array.isArray(payload.wallet)) {
368
+ if (typeof payload.wallet.id === "string" || typeof payload.wallet.id === "number") return String(payload.wallet.id);
369
+ if (typeof payload.wallet.walletId === "string" || typeof payload.wallet.walletId === "number") return String(payload.wallet.walletId);
370
+ }
371
+ if (payload.data && typeof payload.data === "object" && !Array.isArray(payload.data)) {
372
+ if (typeof payload.data.id === "string" || typeof payload.data.id === "number") return String(payload.data.id);
373
+ if (typeof payload.data.walletId === "string" || typeof payload.data.walletId === "number") return String(payload.data.walletId);
374
+ }
375
+ return null;
376
+ }
377
+
378
+ function extractWalletKeys(payload) {
379
+ const scopes = [];
380
+ if (payload && typeof payload === "object" && !Array.isArray(payload)) {
381
+ scopes.push(payload);
382
+ if (payload.wallet && typeof payload.wallet === "object" && !Array.isArray(payload.wallet)) {
383
+ scopes.push(payload.wallet);
384
+ }
385
+ if (payload.data && typeof payload.data === "object" && !Array.isArray(payload.data)) {
386
+ scopes.push(payload.data);
387
+ }
388
+ }
389
+
390
+ const pick = (keys) => {
391
+ for (const scope of scopes) {
392
+ for (const key of keys) {
393
+ const val = scope[key];
394
+ if (typeof val === "string" && val.length > 0) return val;
395
+ }
396
+ }
397
+ return undefined;
398
+ };
399
+
400
+ return {
401
+ publicKey: pick(["walletPublicKey", "publicKey", "address"]),
402
+ privateKey: pick(["walletPrivateKey", "privateKey", "secretKey"]),
403
+ };
404
+ }
405
+
406
+ const BS58_CHARS = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
407
+
408
+ function b58Decode(str) {
409
+ let num = BigInt(0);
410
+ for (const c of str) {
411
+ const idx = BS58_CHARS.indexOf(c);
412
+ if (idx < 0) throw new Error(`Invalid base58 character: ${c}`);
413
+ num = num * 58n + BigInt(idx);
414
+ }
415
+ const hex = num.toString(16);
416
+ const paddedHex = hex.length % 2 ? "0" + hex : hex;
417
+ const bytes = new Uint8Array(paddedHex.length / 2);
418
+ for (let i = 0; i < bytes.length; i++) {
419
+ bytes[i] = parseInt(paddedHex.substring(i * 2, i * 2 + 2), 16);
420
+ }
421
+ let leadingZeros = 0;
422
+ for (const c of str) {
423
+ if (c === "1") leadingZeros++;
424
+ else break;
425
+ }
426
+ if (leadingZeros > 0) {
427
+ const combined = new Uint8Array(leadingZeros + bytes.length);
428
+ combined.set(bytes, leadingZeros);
429
+ return combined;
430
+ }
431
+ return bytes;
432
+ }
433
+
434
+ function b58Encode(bytes) {
435
+ let num = BigInt(0);
436
+ for (const b of bytes) {
437
+ num = num * 256n + BigInt(b);
438
+ }
439
+ let result = "";
440
+ while (num > 0n) {
441
+ result = BS58_CHARS[Number(num % 58n)] + result;
442
+ num = num / 58n;
443
+ }
444
+ for (const b of bytes) {
445
+ if (b === 0) result = "1" + result;
446
+ else break;
447
+ }
448
+ return result || "1";
449
+ }
450
+
451
+ function signChallengeLocally(challengeText, privateKeyBase58) {
452
+ const keyBytes = b58Decode(privateKeyBase58);
453
+ const privKeyRaw = keyBytes.slice(0, 32);
454
+ const pkcs8Prefix = Buffer.from([
455
+ 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06,
456
+ 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20,
457
+ ]);
458
+ const pkcs8Der = Buffer.concat([pkcs8Prefix, Buffer.from(privKeyRaw)]);
459
+ const keyObj = createPrivateKey({ key: pkcs8Der, format: "der", type: "pkcs8" });
460
+ const sig = cryptoSign(null, Buffer.from(challengeText, "utf-8"), keyObj);
461
+ return b58Encode(new Uint8Array(sig));
462
+ }
463
+
464
+ async function doSignup(orchestratorUrl, externalUserId) {
465
+ printInfo(` Signing up as: ${externalUserId}`);
466
+ const res = await httpRequest(`${orchestratorUrl}/api/auth/signup`, {
467
+ method: "POST",
468
+ body: { externalUserId },
469
+ });
470
+
471
+ if (!res.ok) {
472
+ throw new Error(`Signup failed (HTTP ${res.status}): ${JSON.stringify(res.data)}`);
473
+ }
474
+
475
+ return res.data;
476
+ }
477
+
478
+ async function doChallenge(orchestratorUrl, apiKey, walletPublicKey) {
479
+ const body = { apiKey, clientLabel: "openclaw-trader-cli" };
480
+ if (walletPublicKey) body.walletPublicKey = walletPublicKey;
481
+
482
+ const res = await httpRequest(`${orchestratorUrl}/api/session/challenge`, {
483
+ method: "POST",
484
+ body,
485
+ });
486
+
487
+ if (!res.ok) {
488
+ throw new Error(`Challenge request failed (HTTP ${res.status}): ${JSON.stringify(res.data)}`);
489
+ }
490
+
491
+ return res.data;
492
+ }
493
+
494
+ async function doSessionStart(orchestratorUrl, apiKey, challengeId, walletPublicKey, walletSignature) {
495
+ const body = { apiKey, challengeId, clientLabel: "openclaw-trader-cli" };
496
+ if (walletPublicKey) body.walletPublicKey = walletPublicKey;
497
+ if (walletSignature) body.walletSignature = walletSignature;
498
+
499
+ const res = await httpRequest(`${orchestratorUrl}/api/session/start`, {
500
+ method: "POST",
501
+ body,
502
+ });
503
+
504
+ if (!res.ok) {
505
+ throw new Error(`Session start failed (HTTP ${res.status}): ${JSON.stringify(res.data)}`);
506
+ }
507
+
508
+ return res.data;
509
+ }
510
+
511
+ async function doRefresh(orchestratorUrl, refreshToken) {
512
+ const res = await httpRequest(`${orchestratorUrl}/api/session/refresh`, {
513
+ method: "POST",
514
+ body: { refreshToken },
515
+ });
516
+
517
+ if (!res.ok) {
518
+ if (res.status === 401 || res.status === 403) {
519
+ return null;
520
+ }
521
+ throw new Error(`Token refresh failed (HTTP ${res.status}): ${JSON.stringify(res.data)}`);
522
+ }
523
+
524
+ return res.data;
525
+ }
526
+
527
+ async function doRecoverSecret(orchestratorUrl, apiKey, recoverySecret) {
528
+ const res = await httpRequest(`${orchestratorUrl}/api/session/recover-secret`, {
529
+ method: "POST",
530
+ body: { apiKey, recoverySecret, clientLabel: "openclaw-trader-cli" },
531
+ });
532
+ if (!res.ok) {
533
+ throw new Error(`recover-secret failed (HTTP ${res.status}): ${JSON.stringify(res.data)}`);
534
+ }
535
+ return res.data;
536
+ }
537
+
538
+ async function doLogout(orchestratorUrl, refreshToken) {
539
+ const res = await httpRequest(`${orchestratorUrl}/api/session/logout`, {
540
+ method: "POST",
541
+ body: { refreshToken },
542
+ });
543
+ return res.ok;
544
+ }
545
+
546
+ async function establishSession(orchestratorUrl, pluginConfig, walletPrivateKeyInput = "") {
547
+ if (pluginConfig.refreshToken) {
548
+ printInfo(" Refreshing existing session...");
549
+ const tokens = await doRefresh(orchestratorUrl, pluginConfig.refreshToken);
550
+ if (tokens) {
551
+ printSuccess(" Session refreshed successfully");
552
+ pluginConfig.refreshToken = tokens.refreshToken;
553
+ return tokens;
554
+ }
555
+ printWarn(" Refresh token expired. Re-authenticating...");
556
+ }
557
+
558
+ if (!pluginConfig.apiKey) {
559
+ throw new Error("No apiKey configured. Run 'traderclaw setup' first.");
560
+ }
561
+
562
+ if (pluginConfig.recoverySecret) {
563
+ printInfo(" Attempting session recovery via consumable secret...");
564
+ try {
565
+ const tokens = await doRecoverSecret(orchestratorUrl, pluginConfig.apiKey, pluginConfig.recoverySecret);
566
+ pluginConfig.refreshToken = tokens.refreshToken;
567
+ if (tokens.recoverySecret) {
568
+ pluginConfig.recoverySecret = tokens.recoverySecret;
569
+ }
570
+ printSuccess(" Session recovered via consumable secret");
571
+ printInfo(` Tier: ${tokens.session?.tier || "unknown"}`);
572
+ return tokens;
573
+ } catch (err) {
574
+ printWarn(` Consumable recovery failed: ${err.message || err}`);
575
+ printWarn(" Falling back to wallet challenge...");
576
+ }
577
+ }
578
+
579
+ printInfo(" Starting challenge flow...");
580
+ const challenge = await doChallenge(orchestratorUrl, pluginConfig.apiKey, pluginConfig.walletPublicKey);
581
+
582
+ let walletPubKey = undefined;
583
+ let walletSig = undefined;
584
+
585
+ if (challenge.walletProofRequired) {
586
+ printWarn(" Wallet proof required — this account already has a wallet.");
587
+ const walletPrivateKey = getRuntimeWalletPrivateKey(walletPrivateKeyInput);
588
+ if (!walletPrivateKey) {
589
+ printError(` Wallet private key not available. Cannot prove wallet ownership.`);
590
+ printError(` Provide it via --wallet-private-key or env ${WALLET_PRIVATE_KEY_ENV} for local signing.`);
591
+ printSessionTroubleshootingHint();
592
+ throw new Error("Wallet proof required but no private key configured.");
593
+ }
594
+ walletPubKey = challenge.walletPublicKey || pluginConfig.walletPublicKey;
595
+ printInfo(" Signing challenge locally...");
596
+ try {
597
+ walletSig = signChallengeLocally(challenge.challenge, walletPrivateKey);
598
+ printSuccess(" Challenge signed successfully");
599
+ } catch (err) {
600
+ printError(` Failed to sign challenge: ${err.message}`);
601
+ throw new Error("Challenge signing failed. Verify your walletPrivateKey is correct.");
602
+ }
603
+ }
604
+
605
+ const tokens = await doSessionStart(
606
+ orchestratorUrl,
607
+ pluginConfig.apiKey,
608
+ challenge.challengeId,
609
+ walletPubKey,
610
+ walletSig,
611
+ );
612
+
613
+ if (challenge.walletPublicKey) {
614
+ pluginConfig.walletPublicKey = challenge.walletPublicKey;
615
+ }
616
+
617
+ pluginConfig.refreshToken = tokens.refreshToken;
618
+ printSuccess(" Session established");
619
+ printInfo(` Tier: ${tokens.session?.tier || "unknown"}`);
620
+ printInfo(` Scopes: ${(tokens.session?.scopes || []).join(", ")}`);
621
+
622
+ return tokens;
623
+ }
624
+
625
+ async function cmdSetup(args) {
626
+ print(BANNER);
627
+ printInfo("Welcome to TraderClaw V1 setup (session auth).\n");
628
+
629
+ let apiKey = "";
630
+ let orchestratorUrl = "";
631
+ let externalUserId = "";
632
+ let walletPrivateKey = "";
633
+ let gatewayBaseUrl = "";
634
+ let gatewayToken = "";
635
+ let skipGatewayRegistration = false;
636
+ let showApiKey = false;
637
+ let showWalletPrivateKey = false;
638
+ let doSignupFlow = false;
639
+ let signedUpThisSession = false;
640
+ let writeGatewayEnvFlag = false;
641
+ let noEnsureGatewayPersistent = false;
642
+ let signupRecoverySecret = undefined;
643
+
644
+ for (let i = 0; i < args.length; i++) {
645
+ if ((args[i] === "--api-key" || args[i] === "-k") && args[i + 1]) {
646
+ apiKey = args[++i];
647
+ }
648
+ if ((args[i] === "--url" || args[i] === "-u") && args[i + 1]) {
649
+ orchestratorUrl = args[++i];
650
+ }
651
+ if ((args[i] === "--user-id") && args[i + 1]) {
652
+ externalUserId = args[++i];
653
+ }
654
+ if (args[i] === "--wallet-private-key" && args[i + 1]) {
655
+ walletPrivateKey = args[++i];
656
+ }
657
+ if ((args[i] === "--gateway-base-url" || args[i] === "-g") && args[i + 1]) {
658
+ gatewayBaseUrl = args[++i];
659
+ }
660
+ if ((args[i] === "--gateway-token" || args[i] === "-t") && args[i + 1]) {
661
+ gatewayToken = args[++i];
662
+ }
663
+ if (args[i] === "--skip-gateway-registration") {
664
+ skipGatewayRegistration = true;
665
+ }
666
+ if (args[i] === "--show-api-key") {
667
+ showApiKey = true;
668
+ }
669
+ if (args[i] === "--show-wallet-private-key") {
670
+ showWalletPrivateKey = true;
671
+ }
672
+ if (args[i] === "--signup") {
673
+ doSignupFlow = true;
674
+ }
675
+ if (args[i] === "--write-gateway-env") {
676
+ writeGatewayEnvFlag = true;
677
+ }
678
+ if (args[i] === "--no-ensure-gateway-persistent") {
679
+ noEnsureGatewayPersistent = true;
680
+ }
681
+ }
682
+ const runtimeWalletPrivateKey = getRuntimeWalletPrivateKey(walletPrivateKey);
683
+
684
+ if (!orchestratorUrl) {
685
+ orchestratorUrl = await prompt("Orchestrator URL", "https://api.traderclaw.ai");
686
+ }
687
+ orchestratorUrl = orchestratorUrl.replace(/\/+$/, "");
688
+
689
+ if (!apiKey) {
690
+ const hasKey = await confirm("Do you already have a TraderClaw API key?");
691
+ if (hasKey) {
692
+ apiKey = await prompt("Enter your TraderClaw API key");
693
+ } else {
694
+ doSignupFlow = true;
695
+ }
696
+ }
697
+
698
+ if (doSignupFlow) {
699
+ print("\n Signing up for a new account...\n");
700
+ if (!externalUserId) {
701
+ externalUserId = await prompt("External User ID (or press enter for auto-generated)", `agent_${randomUUID().slice(0, 8)}`);
702
+ }
703
+
704
+ for (let signupAttempt = 0; ; signupAttempt++) {
705
+ try {
706
+ const signupResult = await doSignup(orchestratorUrl, externalUserId);
707
+ apiKey = signupResult.apiKey;
708
+ if (signupResult.recoverySecret) {
709
+ signupRecoverySecret = signupResult.recoverySecret;
710
+ }
711
+ signedUpThisSession = true;
712
+ printSuccess(` Signup successful!`);
713
+ printInfo(` Tier: ${signupResult.tier}`);
714
+ printInfo(` Scopes: ${signupResult.scopes.join(", ")}`);
715
+ break;
716
+ } catch (err) {
717
+ const msg = err.message || String(err);
718
+ if (msg.includes("SIGNUP_ALREADY_COMPLETED") || msg.includes("409")) {
719
+ printWarn(` User "${externalUserId}" is already registered.`);
720
+ if (signupAttempt >= 2) {
721
+ printError(" Too many attempts. If you already have an account, re-run setup and choose 'y' when asked for an API key.");
722
+ process.exit(1);
723
+ }
724
+ externalUserId = await prompt("Try a different User ID", `agent_${randomUUID().slice(0, 8)}`);
725
+ } else {
726
+ printError(`Signup failed: ${msg}`);
727
+ process.exit(1);
728
+ }
729
+ }
730
+ }
731
+ }
732
+
733
+ if (signedUpThisSession && apiKey) {
734
+ print("\n" + "=".repeat(60));
735
+ printWarn(" IMPORTANT: Save your TraderClaw API key");
736
+ print("=".repeat(60));
737
+ printInfo(` Preview (masked): ${maskKey(apiKey)}`);
738
+ printWarn(" Your FULL TraderClaw API key is on the next line. Copy it to a password manager before continuing.");
739
+ printWarn(` TraderClaw API Key: ${apiKey}`);
740
+ printWarn(" You will need this key on new machines, for recovery, and for some CLI flows.");
741
+ printWarn(" After setup, it is also saved in your local OpenClaw plugin config.");
742
+ if (showApiKey) {
743
+ printInfo(" (--show-api-key: full key is already shown above.)");
744
+ }
745
+ for (let attempt = 0; ; attempt++) {
746
+ const ack = await prompt("Type API_KEY_STORED to confirm you saved this key", "");
747
+ if (ack === "API_KEY_STORED") break;
748
+ if (attempt >= 2) {
749
+ printError("Confirmation not provided after 3 attempts. Aborting setup so you do not lose access to your API key.");
750
+ process.exit(1);
751
+ }
752
+ printWarn(" Please type exactly: API_KEY_STORED");
753
+ }
754
+ printSuccess(" API key backup confirmation received.");
755
+ }
756
+
757
+ if (!apiKey) {
758
+ printError("API key is required. Use --signup to create an account or provide a key.");
759
+ process.exit(1);
760
+ }
761
+
762
+ print("\nEstablishing session...\n");
763
+
764
+ const existingForRecovery = readConfig();
765
+ const prevPlugin = getPluginConfig(existingForRecovery);
766
+ const pluginConfig = {
767
+ orchestratorUrl,
768
+ walletId: null,
769
+ apiKey,
770
+ apiTimeout: 120000,
771
+ refreshToken: undefined,
772
+ walletPublicKey: undefined,
773
+ agentId: "main",
774
+ recoverySecret: signupRecoverySecret ?? prevPlugin?.recoverySecret,
775
+ };
776
+
777
+ let lastSeenWalletPrivateKey = runtimeWalletPrivateKey || "";
778
+ let sessionTokens;
779
+ try {
780
+ sessionTokens = await establishSession(orchestratorUrl, pluginConfig, runtimeWalletPrivateKey);
781
+ } catch (err) {
782
+ printError(`Session establishment failed: ${err.message}`);
783
+ if (String(err.message || "").includes("Wallet proof")) {
784
+ printSessionTroubleshootingHint();
785
+ }
786
+ printWarn("Saving config without session. You can retry with: traderclaw login");
787
+
788
+ const existingConfig = readConfig();
789
+ setPluginConfig(existingConfig, pluginConfig);
790
+ writeConfig(existingConfig);
791
+ printInfo(` Config saved to ${CONFIG_FILE}`);
792
+ process.exit(1);
793
+ }
794
+
795
+ print("\nChecking system health...\n");
796
+
797
+ const accessToken = sessionTokens.accessToken;
798
+
799
+ try {
800
+ const health = await httpRequest(`${orchestratorUrl}/healthz`, { accessToken });
801
+ if (health.ok) {
802
+ const h = health.data;
803
+ printSuccess(" Orchestrator reachable");
804
+ printInfo(` Service: ${h.service || "unknown"}`);
805
+ printInfo(` Execution mode: ${h.executionMode || "unknown"}`);
806
+ printInfo(` Upstream configured: ${h.upstreamConfigured ? "yes" : "no"}`);
807
+ } else {
808
+ printWarn(` Orchestrator health check returned HTTP ${health.status}`);
809
+ }
810
+ } catch (err) {
811
+ printWarn(` Health check failed: ${err.message}`);
812
+ }
813
+
814
+ print("\nSetting up wallet...\n");
815
+
816
+ let walletId = null;
817
+ let walletLabel = "";
818
+ let createdNewWallet = false;
819
+
820
+ try {
821
+ const walletsRes = await httpRequest(`${orchestratorUrl}/api/wallets`, { accessToken });
822
+ if (walletsRes.ok && Array.isArray(walletsRes.data) && walletsRes.data.length > 0) {
823
+ const wallets = walletsRes.data;
824
+ printInfo(` Found ${wallets.length} existing wallet(s):`);
825
+ wallets.forEach((w, i) => {
826
+ print(` ${i + 1}. ${w.label || "Unnamed"} (ID: ${w.id}, Status: ${w.status})`);
827
+ });
828
+
829
+ const choice = await prompt("\nUse existing wallet? Enter number or 'new' to create one", "1");
830
+
831
+ if (choice.toLowerCase() === "new") {
832
+ walletLabel = await prompt("Wallet label", "Trading Wallet");
833
+ const createRes = await httpRequest(`${orchestratorUrl}/api/wallet/create`, {
834
+ method: "POST",
835
+ body: { label: walletLabel, strategyProfile: "aggressive", includePrivateKey: true },
836
+ accessToken,
837
+ });
838
+ if (createRes.ok) {
839
+ createdNewWallet = true;
840
+ walletId = extractWalletId(createRes.data);
841
+ if (!walletId) {
842
+ throw new Error(`Wallet create response missing wallet ID: ${JSON.stringify(createRes.data)}`);
843
+ }
844
+ const keys = extractWalletKeys(createRes.data);
845
+ if (keys.publicKey) pluginConfig.walletPublicKey = keys.publicKey;
846
+ if (keys.privateKey) lastSeenWalletPrivateKey = keys.privateKey;
847
+ printSuccess(` Wallet created (ID: ${walletId})`);
848
+ } else {
849
+ printError("Failed to create wallet");
850
+ printError(JSON.stringify(createRes.data));
851
+ process.exit(1);
852
+ }
853
+ } else {
854
+ const idx = parseInt(choice, 10) - 1;
855
+ if (idx >= 0 && idx < wallets.length) {
856
+ walletId = extractWalletId(wallets[idx]) || String(wallets[idx].id);
857
+ walletLabel = wallets[idx].label || "Unnamed";
858
+ const keys = extractWalletKeys(wallets[idx]);
859
+ if (keys.publicKey) pluginConfig.walletPublicKey = keys.publicKey;
860
+ if (keys.privateKey) lastSeenWalletPrivateKey = keys.privateKey;
861
+ printSuccess(` Using wallet: ${walletLabel} (ID: ${walletId})`);
862
+ } else {
863
+ walletId = extractWalletId(wallets[0]) || String(wallets[0].id);
864
+ walletLabel = wallets[0].label || "Unnamed";
865
+ const keys = extractWalletKeys(wallets[0]);
866
+ if (keys.publicKey) pluginConfig.walletPublicKey = keys.publicKey;
867
+ if (keys.privateKey) lastSeenWalletPrivateKey = keys.privateKey;
868
+ printSuccess(` Using wallet: ${walletLabel} (ID: ${walletId})`);
869
+ }
870
+ }
871
+ } else {
872
+ printInfo(" No existing wallets found. Creating one...");
873
+ walletLabel = await prompt("Wallet label", "Trading Wallet");
874
+ const createRes = await httpRequest(`${orchestratorUrl}/api/wallet/create`, {
875
+ method: "POST",
876
+ body: { label: walletLabel, strategyProfile: "aggressive", includePrivateKey: true },
877
+ accessToken,
878
+ });
879
+ if (createRes.ok) {
880
+ createdNewWallet = true;
881
+ walletId = extractWalletId(createRes.data);
882
+ if (!walletId) {
883
+ throw new Error(`Wallet create response missing wallet ID: ${JSON.stringify(createRes.data)}`);
884
+ }
885
+ const keys = extractWalletKeys(createRes.data);
886
+ if (keys.publicKey) pluginConfig.walletPublicKey = keys.publicKey;
887
+ if (keys.privateKey) lastSeenWalletPrivateKey = keys.privateKey;
888
+ printSuccess(` Wallet created (ID: ${walletId})`);
889
+ } else {
890
+ printError("Failed to create wallet");
891
+ printError(JSON.stringify(createRes.data));
892
+ process.exit(1);
893
+ }
894
+ }
895
+ } catch (err) {
896
+ printError("Failed to set up wallet");
897
+ printError(err.message || String(err));
898
+ process.exit(1);
899
+ }
900
+
901
+ pluginConfig.walletId = walletId;
902
+
903
+ if (createdNewWallet) {
904
+ print("\n" + "=".repeat(60));
905
+ printWarn(" IMPORTANT: New wallet credentials");
906
+ print("=".repeat(60));
907
+ print(` Wallet Public Key: ${pluginConfig.walletPublicKey || "not returned by API"}`);
908
+ if (lastSeenWalletPrivateKey) {
909
+ printWarn(` Wallet Private Key: ${lastSeenWalletPrivateKey}`);
910
+ printWarn(" Save this private key now in a secure password manager.");
911
+ printWarn(" You may not be able to retrieve this private key again.");
912
+ printWarn(` For wallet proof signing, provide it at runtime via --wallet-private-key or ${WALLET_PRIVATE_KEY_ENV}.`);
913
+ printWarn(" It is NOT saved to openclaw.json.");
914
+ } else {
915
+ printWarn(" Wallet private key was not returned by the API.");
916
+ printWarn(" If this is expected custody behavior, backup via your wallet provider.");
917
+ }
918
+
919
+ if (lastSeenWalletPrivateKey) {
920
+ for (let attempt = 0; ; attempt++) {
921
+ const ack = await prompt("Type BACKED_UP to continue", "");
922
+ if (ack === "BACKED_UP") break;
923
+ if (attempt >= 2) {
924
+ printError("Backup confirmation not provided after 3 attempts. Aborting setup to prevent key loss.");
925
+ process.exit(1);
926
+ }
927
+ printWarn(" Please type exactly: BACKED_UP");
928
+ }
929
+ printSuccess(" Backup confirmation received.");
930
+ }
931
+ }
932
+
933
+ // Re-authenticate WITH wallet proof so the saved refreshToken is accepted by
934
+ // the server after the account has a wallet. Without this, the gateway gets a
935
+ // token issued pre-wallet that the server may reject on refresh.
936
+ if (lastSeenWalletPrivateKey && pluginConfig.walletPublicKey) {
937
+ print("\nStrengthening session with wallet proof...\n");
938
+ try {
939
+ pluginConfig.refreshToken = undefined;
940
+ sessionTokens = await establishSession(orchestratorUrl, pluginConfig, lastSeenWalletPrivateKey);
941
+ printSuccess(" Session re-established with wallet proof.");
942
+ } catch (err) {
943
+ printWarn(` Wallet-proof re-auth skipped: ${err.message}`);
944
+ printWarn(` The gateway may need ${WALLET_PRIVATE_KEY_ENV} in its service environment.`);
945
+ }
946
+ }
947
+
948
+ print("\nWriting configuration...\n");
949
+
950
+ const existingConfig = readConfig();
951
+ removeLegacyWalletPrivateKey(pluginConfig);
952
+ setPluginConfig(existingConfig, pluginConfig);
953
+
954
+ if (!existingConfig.agents || typeof existingConfig.agents !== "object") {
955
+ existingConfig.agents = {};
956
+ }
957
+ if (!existingConfig.agents.defaults) existingConfig.agents.defaults = {};
958
+ if (!existingConfig.agents.defaults.heartbeat || typeof existingConfig.agents.defaults.heartbeat !== "object") {
959
+ existingConfig.agents.defaults.heartbeat = {};
960
+ }
961
+ if (!Array.isArray(existingConfig.agents.list)) {
962
+ existingConfig.agents.list = [];
963
+ }
964
+ const heartbeatPrompt =
965
+ "Read HEARTBEAT.md (workspace context). Follow it strictly — execute the full trading cycle and report results to the user. Do NOT reply HEARTBEAT_OK. Always produce a visible summary of what you checked and did.";
966
+ const defaultHeartbeatEvery = "30m";
967
+ const hasMainAgent = existingConfig.agents.list.some((a) => a && a.id === "main");
968
+ if (!hasMainAgent) {
969
+ existingConfig.agents.list.push({ id: "main", default: true, heartbeat: { every: defaultHeartbeatEvery, target: "last", prompt: heartbeatPrompt } });
970
+ } else {
971
+ const mainAgent = existingConfig.agents.list.find((a) => a.id === "main");
972
+ if (!mainAgent.heartbeat) mainAgent.heartbeat = { every: defaultHeartbeatEvery, target: "last", prompt: heartbeatPrompt };
973
+ else mainAgent.heartbeat.prompt = heartbeatPrompt;
974
+ }
975
+ if (!existingConfig.cron || typeof existingConfig.cron !== "object") {
976
+ existingConfig.cron = { enabled: true, maxConcurrentRuns: 2, sessionRetention: "24h" };
977
+ }
978
+
979
+ writeConfig(existingConfig);
980
+
981
+ printSuccess(` Config written to ${CONFIG_FILE}`);
982
+
983
+ if (!skipGatewayRegistration) {
984
+ print("\nGateway forwarding setup (required for event-driven wakeups)...\n");
985
+
986
+ const defaults = buildGatewayDefaults(existingConfig);
987
+
988
+ if (!gatewayBaseUrl) gatewayBaseUrl = defaults.gatewayBaseUrl || "";
989
+ if (!gatewayToken) gatewayToken = defaults.gatewayToken || "";
990
+
991
+ if (gatewayBaseUrl) {
992
+ printInfo(` Suggested gatewayBaseUrl: ${gatewayBaseUrl}`);
993
+ } else if (defaults.bind === "loopback" && defaults.tailscaleMode === "off") {
994
+ printWarn(" Gateway appears local-only (loopback + tailscale off).");
995
+ printWarn(" For orchestrator callbacks, expose it first, e.g.:");
996
+ printWarn(" openclaw gateway restart --bind tailnet --tailscale serve");
997
+ }
998
+
999
+ if (!gatewayBaseUrl) {
1000
+ gatewayBaseUrl = await prompt("Gateway base URL (public HTTPS URL reachable by orchestrator)");
1001
+ }
1002
+ if (!gatewayToken) {
1003
+ gatewayToken = await prompt("Gateway bearer token (press Enter to use API key)", apiKey);
1004
+ } else {
1005
+ printInfo(` Using gateway token from local config: ${maskKey(gatewayToken)}`);
1006
+ }
1007
+
1008
+ gatewayBaseUrl = (gatewayBaseUrl || "").replace(/\/+$/, "");
1009
+ gatewayToken = (gatewayToken || "").trim();
1010
+
1011
+ if (/localhost|127\.0\.0\.1/i.test(gatewayBaseUrl)) {
1012
+ print("");
1013
+ printWarn(" ╔══════════════════════════════════════════════════════════╗");
1014
+ printWarn(" ║ WARNING: localhost gateway URL detected ║");
1015
+ printWarn(" ╠══════════════════════════════════════════════════════════╣");
1016
+ printWarn(" ║ The orchestrator runs on a remote server and cannot ║");
1017
+ printWarn(" ║ reach localhost on your machine. Event forwarding ║");
1018
+ printWarn(" ║ (alpha signals, Bitquery events) will FAIL. ║");
1019
+ printWarn(" ║ ║");
1020
+ printWarn(" ║ Use a publicly reachable URL instead: ║");
1021
+ printWarn(" ║ - Tailscale: https://gateway.yourname.ts.net ║");
1022
+ printWarn(" ║ - Ngrok: https://abc123.ngrok.io ║");
1023
+ printWarn(" ║ - VPS/Cloud: https://gateway.yourdomain.com ║");
1024
+ printWarn(" ╚══════════════════════════════════════════════════════════╝");
1025
+ print("");
1026
+ }
1027
+
1028
+ if (!gatewayBaseUrl || !gatewayToken) {
1029
+ printError(" gatewayBaseUrl and gatewayToken are required for registration.");
1030
+ printWarn(" Setup is stopping because gateway credentials are mandatory for event-driven startup.");
1031
+ printWarn(" Fix exposure/token and rerun: traderclaw setup");
1032
+ process.exit(1);
1033
+ }
1034
+
1035
+ const putRes = await httpRequest(`${orchestratorUrl}/api/agents/gateway-credentials`, {
1036
+ method: "PUT",
1037
+ body: { gatewayBaseUrl, gatewayToken },
1038
+ accessToken,
1039
+ });
1040
+ if (!putRes.ok) {
1041
+ printError(` Credential registration failed (HTTP ${putRes.status}): ${JSON.stringify(putRes.data)}`);
1042
+ printWarn(" Setup is stopping because gateway credentials are mandatory for event-driven startup.");
1043
+ printWarn(" Fix exposure/token and rerun: traderclaw setup");
1044
+ process.exit(1);
1045
+ }
1046
+
1047
+ const getRes = await httpRequest(`${orchestratorUrl}/api/agents/gateway-credentials`, { accessToken });
1048
+ if (!getRes.ok) {
1049
+ printError(` Credential verification failed (HTTP ${getRes.status}): ${JSON.stringify(getRes.data)}`);
1050
+ printWarn(" Setup is stopping because gateway credentials are mandatory for event-driven startup.");
1051
+ printWarn(" Fix exposure/token and rerun: traderclaw setup");
1052
+ process.exit(1);
1053
+ }
1054
+
1055
+ const credentialList = Array.isArray(getRes.data?.credentials) ? getRes.data.credentials : [];
1056
+ const normalizedGatewayBaseUrl = (gatewayBaseUrl || "").replace(/\/+$/, "");
1057
+ let matchingCredentials = credentialList.filter((entry) => {
1058
+ const entryUrl = typeof entry?.gatewayBaseUrl === "string" ? entry.gatewayBaseUrl.replace(/\/+$/, "") : "";
1059
+ return entryUrl && entryUrl === normalizedGatewayBaseUrl;
1060
+ });
1061
+ if (matchingCredentials.length === 0) {
1062
+ matchingCredentials = credentialList;
1063
+ }
1064
+
1065
+ const hasActiveCredential = matchingCredentials.some((entry) => entry && entry.active === true);
1066
+ const legacyActive = getNestedBool(getRes.data, "active");
1067
+ const active = hasActiveCredential || legacyActive === true;
1068
+
1069
+ if (!active) {
1070
+ printError(
1071
+ ` Credential verification did not find an active credential (matched=${matchingCredentials.length}, total=${credentialList.length}, legacyActive=${String(legacyActive)}).`,
1072
+ );
1073
+ printWarn(" The orchestrator could not confirm reachability of your gateway URL.");
1074
+ printWarn(" Ensure the URL is publicly reachable and rerun: traderclaw setup");
1075
+ process.exit(1);
1076
+ }
1077
+
1078
+ printSuccess(" Gateway credentials registered and active.");
1079
+ printInfo(` Registered gatewayBaseUrl: ${gatewayBaseUrl}`);
1080
+
1081
+ pluginConfig.gatewayBaseUrl = gatewayBaseUrl;
1082
+ pluginConfig.gatewayToken = gatewayToken;
1083
+ setPluginConfig(existingConfig, pluginConfig);
1084
+ writeConfig(existingConfig);
1085
+ } else {
1086
+ printWarn(" Gateway registration was skipped (--skip-gateway-registration).");
1087
+ printWarn(" The startup sequence will block before trading until credentials are active.");
1088
+ }
1089
+
1090
+ if (writeGatewayEnvFlag && lastSeenWalletPrivateKey) {
1091
+ if (process.platform !== "linux" || process.env.WSL_DISTRO_NAME) {
1092
+ printWarn(
1093
+ " --write-gateway-env is for Linux (non-WSL) systemd user gateways; skipped on this platform.",
1094
+ );
1095
+ } else {
1096
+ try {
1097
+ const { writeTraderclawGatewayWalletEnv } = await import("./gateway-persistence-linux.mjs");
1098
+ writeTraderclawGatewayWalletEnv(lastSeenWalletPrivateKey);
1099
+ printSuccess(
1100
+ ` Wrote ${WALLET_PRIVATE_KEY_ENV} for the systemd user gateway (see ~/.config/systemd/user/openclaw-gateway.service.d/).`,
1101
+ );
1102
+ printInfo(" Run: openclaw gateway restart");
1103
+ } catch (err) {
1104
+ printWarn(` Could not write gateway wallet env file: ${err.message || err}`);
1105
+ }
1106
+ }
1107
+ } else if (writeGatewayEnvFlag && !lastSeenWalletPrivateKey) {
1108
+ printWarn(" --write-gateway-env skipped: no wallet private key was available this session.");
1109
+ }
1110
+
1111
+ if (!noEnsureGatewayPersistent) {
1112
+ try {
1113
+ const { ensureLinuxGatewayPersistence, isLinuxGatewayPersistenceEligible } = await import(
1114
+ "./gateway-persistence-linux.mjs",
1115
+ );
1116
+ if (isLinuxGatewayPersistenceEligible()) {
1117
+ print("\nGateway persistence (Linux)...\n");
1118
+ await ensureLinuxGatewayPersistence({
1119
+ emitLog: (level, text) => {
1120
+ if (level === "warn") printWarn(` ${text}`);
1121
+ else printInfo(` ${text}`);
1122
+ },
1123
+ });
1124
+ }
1125
+ } catch (err) {
1126
+ printWarn(` Gateway persistence (optional): ${err.message || err}`);
1127
+ }
1128
+ }
1129
+
1130
+ try {
1131
+ const { deployWorkspaceHeartbeat } = await import("./installer-step-engine.mjs");
1132
+ print("\nWorkspace HEARTBEAT.md...\n");
1133
+ const hb = deployWorkspaceHeartbeat({ pluginPackage: NPM_PACKAGE_NAME });
1134
+ if (hb.deployed) {
1135
+ printSuccess(` Installed HEARTBEAT.md → ${hb.dest}`);
1136
+ } else if (hb.skipped) {
1137
+ printInfo(` HEARTBEAT.md already exists at ${hb.dest} — left unchanged.`);
1138
+ } else {
1139
+ printWarn(` Could not install HEARTBEAT.md automatically (${hb.reason || "unknown"})`);
1140
+ if (hb.src) printInfo(` Expected source: ${hb.src}`);
1141
+ }
1142
+ } catch (err) {
1143
+ printWarn(` HEARTBEAT.md workspace install: ${err.message || err}`);
1144
+ }
1145
+
1146
+ print("\n" + "=".repeat(60));
1147
+ printSuccess("\n Setup complete!\n");
1148
+ print("=".repeat(60));
1149
+ print(`
1150
+ Orchestrator: ${orchestratorUrl}
1151
+ Wallet: ${walletLabel} (ID: ${walletId})
1152
+ Wallet PubKey: ${pluginConfig.walletPublicKey || "not set"}
1153
+ Wallet PrivKey:${lastSeenWalletPrivateKey ? (createdNewWallet || showWalletPrivateKey ? " " + lastSeenWalletPrivateKey : " " + maskKey(lastSeenWalletPrivateKey)) : " not saved"}
1154
+ Gateway URL: ${gatewayBaseUrl || "not set"}
1155
+ Gateway Token: ${gatewayToken ? maskKey(gatewayToken) : "not set"}
1156
+ API Key: ${showApiKey ? apiKey : maskKey(apiKey)}
1157
+ Session: Active (tier: ${sessionTokens.session?.tier || "?"})
1158
+ Config: ${CONFIG_FILE}
1159
+ `);
1160
+ print(` Runtime wallet proof key source: --wallet-private-key or env ${WALLET_PRIVATE_KEY_ENV} (never openclaw.json)`);
1161
+ printWarn(
1162
+ ` For the OpenClaw gateway (Telegram/agent tools), the same env must be set on the gateway service — not only in this shell. See: ${TRADERCLAW_SESSION_TROUBLESHOOTING_URL}`,
1163
+ );
1164
+ print("Session commands:");
1165
+ print(" traderclaw status Check connection health (auto-refreshes session)");
1166
+ print(" traderclaw login Re-authenticate (challenge flow)");
1167
+ print(" traderclaw logout Revoke current session");
1168
+ print(" traderclaw config View current configuration");
1169
+ print("");
1170
+ }
1171
+
1172
+ async function cmdGateway(args) {
1173
+ const sub = args[0];
1174
+ if (sub === "ensure-persistent") {
1175
+ const { ensureLinuxGatewayPersistence } = await import("./gateway-persistence-linux.mjs");
1176
+ print("\nTraderClaw — gateway persistence (Linux)\n");
1177
+ const result = await ensureLinuxGatewayPersistence({
1178
+ emitLog: (level, text) => {
1179
+ if (level === "warn") printWarn(` ${text}`);
1180
+ else printInfo(` ${text}`);
1181
+ },
1182
+ });
1183
+ if (result.skipped) {
1184
+ printInfo(` Skipped: ${result.reason || "not applicable"}`);
1185
+ return;
1186
+ }
1187
+ if (result.errors?.length) {
1188
+ printWarn(` Completed with notes: ${result.errors.join("; ")}`);
1189
+ } else {
1190
+ printSuccess(" Done. Gateway should survive SSH disconnect; use: openclaw gateway restart");
1191
+ }
1192
+ return;
1193
+ }
1194
+ printError("Unknown gateway subcommand. Try: traderclaw gateway ensure-persistent");
1195
+ process.exit(1);
1196
+ }
1197
+
1198
+ async function cmdLogin(args) {
1199
+ const config = readConfig();
1200
+ const pluginConfig = getPluginConfig(config);
1201
+
1202
+ if (!pluginConfig) {
1203
+ printError("No plugin configuration found. Run 'traderclaw setup' first.");
1204
+ process.exit(1);
1205
+ }
1206
+
1207
+ const orchestratorUrl = pluginConfig.orchestratorUrl;
1208
+
1209
+ if (!orchestratorUrl) {
1210
+ printError("orchestratorUrl not set. Run 'traderclaw setup' first.");
1211
+ process.exit(1);
1212
+ }
1213
+
1214
+ if (!pluginConfig.apiKey) {
1215
+ printError("apiKey not set. Run 'traderclaw signup' or 'traderclaw setup --signup' for a new account, or 'traderclaw setup' to enter an existing key.");
1216
+ process.exit(1);
1217
+ }
1218
+
1219
+ print("\nTraderClaw V1 - Login\n");
1220
+ print("=".repeat(45));
1221
+
1222
+ let walletPrivateKeyArg = "";
1223
+ let forceReauth = false;
1224
+ for (let i = 0; i < args.length; i++) {
1225
+ if (args[i] === "--wallet-private-key" && args[i + 1]) {
1226
+ walletPrivateKeyArg = args[++i];
1227
+ }
1228
+ if (args[i] === "--force-reauth") {
1229
+ forceReauth = true;
1230
+ }
1231
+ }
1232
+
1233
+ if (forceReauth) {
1234
+ pluginConfig.refreshToken = undefined;
1235
+ printInfo(" --force-reauth: starting full challenge (refresh token cleared).");
1236
+ }
1237
+
1238
+ const removedLegacyKey = removeLegacyWalletPrivateKey(pluginConfig);
1239
+
1240
+ try {
1241
+ await establishSession(orchestratorUrl, pluginConfig, walletPrivateKeyArg);
1242
+ setPluginConfig(config, pluginConfig);
1243
+ writeConfig(config);
1244
+ printSuccess("\n Session established and saved.");
1245
+ if (removedLegacyKey) {
1246
+ printWarn(" Removed deprecated walletPrivateKey from openclaw.json.");
1247
+ }
1248
+ printInfo(` For wallet proof after refresh expires, pass --wallet-private-key or set ${WALLET_PRIVATE_KEY_ENV} (gateway service needs the env too — see docs).`);
1249
+ print(" Restart the gateway for changes to take effect: openclaw gateway --restart");
1250
+ print(" Full re-challenge (e.g. after logout): traderclaw login --force-reauth\n");
1251
+ } catch (err) {
1252
+ printError(`Login failed: ${err.message}`);
1253
+ if (String(err.message || "").includes("Wallet proof") || String(err.message || "").includes("private key")) {
1254
+ printSessionTroubleshootingHint();
1255
+ }
1256
+ process.exit(1);
1257
+ }
1258
+ }
1259
+
1260
+ async function cmdLogout() {
1261
+ const config = readConfig();
1262
+ const pluginConfig = getPluginConfig(config);
1263
+
1264
+ if (!pluginConfig) {
1265
+ printError("No plugin configuration found.");
1266
+ process.exit(1);
1267
+ }
1268
+
1269
+ const orchestratorUrl = pluginConfig.orchestratorUrl;
1270
+
1271
+ print("\nTraderClaw V1 - Logout\n");
1272
+
1273
+ if (pluginConfig.refreshToken) {
1274
+ try {
1275
+ const ok = await doLogout(orchestratorUrl, pluginConfig.refreshToken);
1276
+ if (ok) {
1277
+ printSuccess(" Session revoked on server.");
1278
+ } else {
1279
+ printWarn(" Server logout returned non-OK (session may already be expired).");
1280
+ }
1281
+ } catch (err) {
1282
+ printWarn(` Server logout failed: ${err.message}`);
1283
+ }
1284
+ }
1285
+
1286
+ pluginConfig.refreshToken = undefined;
1287
+ const removedLegacyKey = removeLegacyWalletPrivateKey(pluginConfig);
1288
+ setPluginConfig(config, pluginConfig);
1289
+ writeConfig(config);
1290
+
1291
+ printSuccess(" Local session cleared.");
1292
+ if (removedLegacyKey) {
1293
+ printWarn(" Removed deprecated walletPrivateKey from openclaw.json.");
1294
+ }
1295
+ print(" Run 'traderclaw login' to re-authenticate (your API key must still be in config).");
1296
+ print(" New account or lost API key: run 'traderclaw signup' or 'traderclaw setup --signup' on this machine — not via the agent.");
1297
+ print(" Wallet challenges are signed locally; the private key never leaves this system.\n");
1298
+ }
1299
+
1300
+ async function cmdStatus() {
1301
+ const config = readConfig();
1302
+ const pluginConfig = getPluginConfig(config);
1303
+
1304
+ if (!pluginConfig) {
1305
+ printError("No plugin configuration found. Run 'traderclaw setup' first.");
1306
+ process.exit(1);
1307
+ }
1308
+
1309
+ const orchestratorUrl = pluginConfig.orchestratorUrl;
1310
+ const walletId = pluginConfig.walletId;
1311
+
1312
+ if (!orchestratorUrl) {
1313
+ printError("orchestratorUrl not set in config. Run 'traderclaw setup' to fix.");
1314
+ process.exit(1);
1315
+ }
1316
+
1317
+ print("\nTraderClaw V1 - Status\n");
1318
+ print("=".repeat(45));
1319
+
1320
+ let accessToken = null;
1321
+
1322
+ if (pluginConfig.refreshToken) {
1323
+ try {
1324
+ const tokens = await doRefresh(orchestratorUrl, pluginConfig.refreshToken);
1325
+ if (tokens) {
1326
+ accessToken = tokens.accessToken;
1327
+ pluginConfig.refreshToken = tokens.refreshToken;
1328
+ removeLegacyWalletPrivateKey(pluginConfig);
1329
+ setPluginConfig(config, pluginConfig);
1330
+ writeConfig(config);
1331
+ printSuccess(" Session: ACTIVE");
1332
+ printInfo(` Tier: ${tokens.session?.tier || "?"}`);
1333
+ printInfo(` Scopes: ${(tokens.session?.scopes || []).join(", ")}`);
1334
+ } else {
1335
+ printWarn(" Session: EXPIRED (run 'traderclaw login')");
1336
+ }
1337
+ } catch (err) {
1338
+ printWarn(` Session: ERROR (${err.message})`);
1339
+ }
1340
+ } else if (pluginConfig.apiKey) {
1341
+ printWarn(" Session: NOT ESTABLISHED (run 'traderclaw login' to authenticate)");
1342
+ } else {
1343
+ printError(" Session: NO CREDENTIALS (run 'traderclaw setup')");
1344
+ }
1345
+
1346
+ const authOpts = accessToken ? { accessToken, timeout: 5000 } : { timeout: 5000 };
1347
+
1348
+ try {
1349
+ const health = await httpRequest(`${orchestratorUrl}/healthz`, authOpts);
1350
+ if (health.ok) {
1351
+ const h = health.data;
1352
+ printSuccess(" Orchestrator: CONNECTED");
1353
+ printInfo(` Execution mode: ${h.executionMode || "unknown"}`);
1354
+ printInfo(` Upstream: ${h.upstreamConfigured ? "configured" : "not configured"}`);
1355
+ } else {
1356
+ printError(` Orchestrator: ERROR (HTTP ${health.status})`);
1357
+ }
1358
+ } catch (err) {
1359
+ printError(" Orchestrator: UNREACHABLE");
1360
+ printError(` ${err.message || String(err)}`);
1361
+ }
1362
+
1363
+ try {
1364
+ const statusRes = await httpRequest(`${orchestratorUrl}/api/system/status`, authOpts);
1365
+ if (statusRes.ok) {
1366
+ const s = statusRes.data;
1367
+ printSuccess(" System status: OK");
1368
+ if (s.wsConnections !== undefined) printInfo(` WS connections: ${s.wsConnections}`);
1369
+ }
1370
+ } catch {
1371
+ printWarn(" System status: unavailable");
1372
+ }
1373
+
1374
+ try {
1375
+ const credsRes = await httpRequest(`${orchestratorUrl}/api/agents/gateway-credentials`, authOpts);
1376
+ if (credsRes.ok && credsRes.data && Array.isArray(credsRes.data.credentials)) {
1377
+ const activeCreds = credsRes.data.credentials.filter((entry) => entry && entry.active);
1378
+ printInfo(` Gateway creds: ${activeCreds.length > 0 ? "active" : "missing/inactive"} (${activeCreds.length})`);
1379
+ if (activeCreds.length > 0) {
1380
+ const primary = activeCreds.find((entry) => (entry.agentId || "main") === (pluginConfig.agentId || "main")) || activeCreds[0];
1381
+ printInfo(` Gateway agent: ${primary.agentId || "default"}`);
1382
+ printInfo(` Gateway lastUsed: ${primary.lastUsedAt || "never"}`);
1383
+ }
1384
+ } else {
1385
+ printWarn(" Gateway creds: unavailable");
1386
+ }
1387
+ } catch {
1388
+ printWarn(" Gateway creds: unavailable");
1389
+ }
1390
+
1391
+ print("");
1392
+
1393
+ if (walletId) {
1394
+ try {
1395
+ const capitalRes = await httpRequest(`${orchestratorUrl}/api/capital/status?walletId=${walletId}`, authOpts);
1396
+ if (capitalRes.ok) {
1397
+ const c = capitalRes.data;
1398
+ printSuccess(" Wallet: ACTIVE");
1399
+ printInfo(` Wallet ID: ${walletId}`);
1400
+ printInfo(` Balance: ${c.balanceSol ?? "?"} SOL`);
1401
+ printInfo(` Open positions: ${c.openPositionCount ?? "?"}`);
1402
+ printInfo(` Unrealized PnL: ${c.totalUnrealizedPnl ?? "?"} SOL`);
1403
+ printInfo(` Daily loss: ${c.dailyLossSol ?? 0} SOL`);
1404
+ } else {
1405
+ printError(" Wallet: ERROR");
1406
+ }
1407
+ } catch {
1408
+ printWarn(" Wallet: unavailable");
1409
+ }
1410
+
1411
+ try {
1412
+ const ksRes = await httpRequest(`${orchestratorUrl}/api/killswitch/status?walletId=${walletId}`, authOpts);
1413
+ if (ksRes.ok) {
1414
+ const ks = ksRes.data;
1415
+ if (ks.enabled) {
1416
+ printWarn(` Kill switch: ENABLED (${ks.mode})`);
1417
+ } else {
1418
+ printInfo(" Kill switch: disabled");
1419
+ }
1420
+ }
1421
+ } catch {
1422
+ /* skip */
1423
+ }
1424
+
1425
+ try {
1426
+ const stratRes = await httpRequest(`${orchestratorUrl}/api/strategy/state?walletId=${walletId}`, authOpts);
1427
+ if (stratRes.ok) {
1428
+ const st = stratRes.data;
1429
+ printInfo(` Strategy version: ${st.strategyVersion || "?"}`);
1430
+ printInfo(` Mode: ${st.mode || "HARDENED"}`);
1431
+ }
1432
+ } catch {
1433
+ /* skip */
1434
+ }
1435
+ }
1436
+
1437
+ print("\n" + "=".repeat(45));
1438
+ print("");
1439
+ }
1440
+
1441
+ async function cmdConfig(subArgs) {
1442
+ const subCmd = subArgs[0] || "show";
1443
+
1444
+ if (subCmd === "show") {
1445
+ const config = readConfig();
1446
+ const pluginConfig = getPluginConfig(config);
1447
+
1448
+ if (!pluginConfig) {
1449
+ printError("No plugin configuration found. Run 'traderclaw setup' first.");
1450
+ process.exit(1);
1451
+ }
1452
+
1453
+ print("\nTraderClaw V1 - Configuration\n");
1454
+ print("=".repeat(45));
1455
+ print(` Config file: ${CONFIG_FILE}`);
1456
+ print(` Orchestrator URL: ${pluginConfig.orchestratorUrl || "not set"}`);
1457
+ print(` Gateway URL: ${pluginConfig.gatewayBaseUrl || "not set"}`);
1458
+ print(` Gateway Token: ${pluginConfig.gatewayToken ? maskKey(pluginConfig.gatewayToken) : "not set"}`);
1459
+ print(` Wallet ID: ${pluginConfig.walletId ?? "not set"}`);
1460
+ print(` API Key: ${pluginConfig.apiKey ? maskKey(pluginConfig.apiKey) : "not set"}`);
1461
+ print(` Refresh Token: ${pluginConfig.refreshToken ? maskKey(pluginConfig.refreshToken) : "not set"}`);
1462
+ print(` Wallet Pub Key: ${pluginConfig.walletPublicKey || "not set"}`);
1463
+ print(` Wallet Priv Key: runtime-only via --wallet-private-key or ${WALLET_PRIVATE_KEY_ENV}`);
1464
+ print(` Agent ID: ${pluginConfig.agentId || "not set"}`);
1465
+ print(` API Timeout: ${pluginConfig.apiTimeout || 120000}ms`);
1466
+ print("=".repeat(45));
1467
+ print("");
1468
+ return;
1469
+ }
1470
+
1471
+ if (subCmd === "set") {
1472
+ const key = subArgs[1];
1473
+ const value = subArgs[2];
1474
+
1475
+ if (!key || !value) {
1476
+ printError("Usage: traderclaw config set <key> <value>");
1477
+ print(" Available keys: orchestratorUrl, walletId, apiKey, apiTimeout, refreshToken, walletPublicKey, gatewayBaseUrl, gatewayToken, agentId");
1478
+ process.exit(1);
1479
+ }
1480
+
1481
+ if (key === "walletPrivateKey") {
1482
+ printError(
1483
+ `walletPrivateKey is no longer stored in openclaw.json. Use --wallet-private-key or env ${WALLET_PRIVATE_KEY_ENV} at runtime instead.`,
1484
+ );
1485
+ process.exit(1);
1486
+ }
1487
+
1488
+ const allowedKeys = ["orchestratorUrl", "walletId", "apiKey", "apiTimeout", "refreshToken", "walletPublicKey", "gatewayBaseUrl", "gatewayToken", "agentId"];
1489
+ if (!allowedKeys.includes(key)) {
1490
+ printError(`Unknown config key: ${key}`);
1491
+ print(` Available keys: ${allowedKeys.join(", ")}`);
1492
+ process.exit(1);
1493
+ }
1494
+
1495
+ const config = readConfig();
1496
+ const pluginConfig = getPluginConfig(config) || {};
1497
+
1498
+ let parsedValue = value;
1499
+ if (key === "walletId") {
1500
+ const num = parseInt(value, 10);
1501
+ parsedValue = isNaN(num) ? value : num;
1502
+ }
1503
+ if (key === "apiTimeout") {
1504
+ parsedValue = parseInt(value, 10);
1505
+ if (isNaN(parsedValue)) {
1506
+ printError("apiTimeout must be a number (milliseconds)");
1507
+ process.exit(1);
1508
+ }
1509
+ }
1510
+
1511
+ removeLegacyWalletPrivateKey(pluginConfig);
1512
+ pluginConfig[key] = parsedValue;
1513
+ setPluginConfig(config, pluginConfig);
1514
+ writeConfig(config);
1515
+
1516
+ const sensitiveKeys = ["apiKey", "refreshToken"];
1517
+ printSuccess(`Set ${key} = ${sensitiveKeys.includes(key) ? maskKey(value) : value}`);
1518
+ print("Restart the gateway for changes to take effect: openclaw gateway --restart");
1519
+ return;
1520
+ }
1521
+
1522
+ if (subCmd === "reset") {
1523
+ const confirmed = await confirm("This will remove all OpenClaw Solana Trader configuration. Continue?");
1524
+ if (!confirmed) {
1525
+ print("Cancelled.");
1526
+ return;
1527
+ }
1528
+
1529
+ const config = readConfig();
1530
+ if (config.plugins && config.plugins.entries) {
1531
+ delete config.plugins.entries[PLUGIN_ID];
1532
+ }
1533
+ writeConfig(config);
1534
+ printSuccess("Plugin configuration removed.");
1535
+ return;
1536
+ }
1537
+
1538
+ printError(`Unknown config subcommand: ${subCmd}`);
1539
+ print(" Available: show, set, reset");
1540
+ process.exit(1);
1541
+ }
1542
+
1543
+ function parseInstallWizardArgs(args) {
1544
+ const out = {
1545
+ port: 17890,
1546
+ lane: "event-driven",
1547
+ apiKey: "",
1548
+ llmProvider: "",
1549
+ llmModel: "",
1550
+ llmCredential: "",
1551
+ orchestratorUrl: "https://api.traderclaw.ai",
1552
+ gatewayBaseUrl: "",
1553
+ gatewayToken: "",
1554
+ enableTelegram: false,
1555
+ telegramToken: "",
1556
+ xConsumerKey: "",
1557
+ xConsumerSecret: "",
1558
+ xAccessTokenMain: "",
1559
+ xAccessTokenMainSecret: "",
1560
+ };
1561
+
1562
+ for (let i = 0; i < args.length; i++) {
1563
+ const key = args[i];
1564
+ const next = args[i + 1];
1565
+ if (key === "--port" && next) out.port = Number.parseInt(args[++i], 10) || 17890;
1566
+ if (key === "--lane" && next) out.lane = next === "quick-local" ? "quick-local" : "event-driven";
1567
+ if ((key === "--api-key" || key === "-k") && next) out.apiKey = args[++i];
1568
+ if (key === "--llm-provider" && next) out.llmProvider = args[++i];
1569
+ if (key === "--llm-model" && next) out.llmModel = args[++i];
1570
+ if ((key === "--llm-api-key" || key === "--llm-token") && next) out.llmCredential = args[++i];
1571
+ if ((key === "--url" || key === "-u") && next) out.orchestratorUrl = args[++i];
1572
+ if ((key === "--gateway-base-url" || key === "-g") && next) out.gatewayBaseUrl = args[++i];
1573
+ if ((key === "--gateway-token" || key === "-t") && next) out.gatewayToken = args[++i];
1574
+ if (key === "--with-telegram") out.enableTelegram = true;
1575
+ if (key === "--telegram-token" && next) out.telegramToken = args[++i];
1576
+ }
1577
+ return out;
1578
+ }
1579
+
1580
+ function parsePrecheckArgs(args) {
1581
+ const out = {
1582
+ mode: "dry-run",
1583
+ outputPath: "",
1584
+ orchestratorUrl: "https://api.traderclaw.ai",
1585
+ expectedNodeMajor: 22,
1586
+ };
1587
+
1588
+ for (let i = 0; i < args.length; i++) {
1589
+ const key = args[i];
1590
+ const next = args[i + 1];
1591
+ if (key === "--allow-install") out.mode = "allow-install";
1592
+ if (key === "--dry-run") out.mode = "dry-run";
1593
+ if (key === "--output" && next) out.outputPath = args[++i];
1594
+ if (key.startsWith("--output=")) out.outputPath = key.slice("--output=".length);
1595
+ if (key === "--url" && next) out.orchestratorUrl = args[++i];
1596
+ if (key === "--expected-node-major" && next) {
1597
+ const parsed = Number.parseInt(args[++i], 10);
1598
+ if (Number.isFinite(parsed) && parsed > 0) out.expectedNodeMajor = parsed;
1599
+ }
1600
+ }
1601
+ return out;
1602
+ }
1603
+
1604
+ function nowIso() {
1605
+ return new Date().toISOString();
1606
+ }
1607
+
1608
+ function appendOutput(outputPath, line) {
1609
+ if (!outputPath) return;
1610
+ appendFileSync(outputPath, `${line}\n`, "utf-8");
1611
+ }
1612
+
1613
+ function makePrecheckLogger(outputPath) {
1614
+ const counters = { pass: 0, fail: 0, warn: 0 };
1615
+ const log = (level, message) => {
1616
+ const line = `${nowIso()} [${level}] ${message}`;
1617
+ print(line);
1618
+ appendOutput(outputPath, line);
1619
+ if (level === "PASS") counters.pass += 1;
1620
+ if (level === "FAIL") counters.fail += 1;
1621
+ if (level === "WARN") counters.warn += 1;
1622
+ };
1623
+ return {
1624
+ counters,
1625
+ info: (m) => log("INFO", m),
1626
+ pass: (m) => log("PASS", m),
1627
+ fail: (m) => log("FAIL", m),
1628
+ warn: (m) => log("WARN", m),
1629
+ };
1630
+ }
1631
+
1632
+ function nodeMajorVersion() {
1633
+ const v = getCommandOutput("node -v");
1634
+ if (!v) return 0;
1635
+ const parsed = Number.parseInt(v.replace(/^v/i, "").split(".")[0], 10);
1636
+ return Number.isFinite(parsed) ? parsed : 0;
1637
+ }
1638
+
1639
+ async function cmdPrecheck(args) {
1640
+ const opts = parsePrecheckArgs(args);
1641
+ if (opts.outputPath) {
1642
+ writeFileSync(opts.outputPath, "", "utf-8");
1643
+ }
1644
+ const log = makePrecheckLogger(opts.outputPath);
1645
+
1646
+ log.info("Starting TraderClaw precheck");
1647
+ log.info(`Mode: ${opts.mode}`);
1648
+ log.info(`Orchestrator URL: ${opts.orchestratorUrl}`);
1649
+
1650
+ if (!commandExists("node")) {
1651
+ log.fail("node exists in PATH");
1652
+ } else {
1653
+ const major = nodeMajorVersion();
1654
+ if (major >= opts.expectedNodeMajor) {
1655
+ log.pass(`node major >= ${opts.expectedNodeMajor} (found v${major})`);
1656
+ } else {
1657
+ log.fail(`node major >= ${opts.expectedNodeMajor} (found v${major})`);
1658
+ }
1659
+ }
1660
+
1661
+ if (commandExists("npm")) log.pass("npm exists in PATH");
1662
+ else log.fail("npm exists in PATH");
1663
+
1664
+ if (commandExists("openclaw")) {
1665
+ log.pass("openclaw exists in PATH");
1666
+ } else if (opts.mode === "allow-install") {
1667
+ log.info("Installing openclaw (allow-install mode)");
1668
+ try {
1669
+ execSync("npm install -g openclaw", { stdio: "ignore" });
1670
+ if (commandExists("openclaw")) log.pass("openclaw installed successfully");
1671
+ else log.fail("openclaw install completed but command is still missing");
1672
+ } catch {
1673
+ log.fail("openclaw install failed");
1674
+ }
1675
+ } else {
1676
+ log.warn("openclaw missing (dry-run mode, not installing)");
1677
+ }
1678
+
1679
+ if (commandExists("tailscale")) {
1680
+ log.pass("tailscale exists in PATH");
1681
+ } else if (opts.mode === "allow-install") {
1682
+ log.info("Installing tailscale (allow-install mode)");
1683
+ try {
1684
+ execSync("bash -lc \"if command -v sudo >/dev/null 2>&1; then sudo bash -lc 'curl -fsSL https://tailscale.com/install.sh | sh'; else curl -fsSL https://tailscale.com/install.sh | sh; fi\"", { stdio: "inherit" });
1685
+ if (commandExists("tailscale")) log.pass("tailscale installed successfully");
1686
+ else log.fail("tailscale install completed but command is still missing");
1687
+ } catch {
1688
+ log.fail("tailscale install failed");
1689
+ log.warn("If this is a sudo/permission issue, run: sudo bash -lc 'curl -fsSL https://tailscale.com/install.sh | sh'");
1690
+ }
1691
+ } else {
1692
+ log.warn("tailscale missing (dry-run mode, not installing)");
1693
+ }
1694
+
1695
+ try {
1696
+ const orchestrator = await httpRequest(`${opts.orchestratorUrl.replace(/\/+$/, "")}/healthz`, { timeout: 10000 });
1697
+ if (orchestrator.ok) log.pass(`orchestrator health endpoint reachable (${opts.orchestratorUrl.replace(/\/+$/, "")}/healthz)`);
1698
+ else log.warn(`orchestrator health endpoint returned HTTP ${orchestrator.status} (${opts.orchestratorUrl.replace(/\/+$/, "")}/healthz)`);
1699
+ } catch {
1700
+ log.warn(`orchestrator health endpoint not reachable (${opts.orchestratorUrl.replace(/\/+$/, "")}/healthz)`);
1701
+ }
1702
+
1703
+ if (commandExists("openclaw")) {
1704
+ try {
1705
+ execSync("openclaw gateway status", { stdio: "ignore" });
1706
+ log.pass("openclaw gateway status command succeeded");
1707
+ } catch {
1708
+ log.warn("openclaw gateway status returned non-zero");
1709
+ }
1710
+ try {
1711
+ const { getLinuxGatewayPersistenceSnapshot } = await import("./gateway-persistence-linux.mjs");
1712
+ const snap = getLinuxGatewayPersistenceSnapshot();
1713
+ if (snap.eligible && snap.linger !== true) {
1714
+ log.warn(
1715
+ "systemd user linger not enabled — gateway may stop after SSH disconnect; run: traderclaw gateway ensure-persistent",
1716
+ );
1717
+ } else if (snap.eligible && snap.linger === true) {
1718
+ log.pass("systemd user linger enabled (SSH-safe gateway)");
1719
+ }
1720
+ } catch {
1721
+ log.warn("could not check systemd user linger (optional)");
1722
+ }
1723
+ } else {
1724
+ log.warn("skipping gateway status check (openclaw missing)");
1725
+ }
1726
+
1727
+ log.info("Manual staging run commands:");
1728
+ log.info(" 1) traderclaw install --wizard");
1729
+ log.info(" 2) In wizard, set LLM provider + credential and Telegram token");
1730
+ log.info(" 3) Approve tailscale login in provided URL");
1731
+ log.info(" 4) Confirm /v1/responses returns non-404 on funnel host");
1732
+ log.info(" 5) Verify Telegram channel setup + probe");
1733
+ log.info(" 6) Startup prompt check: solana_system_status, solana_alpha_subscribe, solana_positions");
1734
+ log.info(`Precheck summary: pass=${log.counters.pass} fail=${log.counters.fail} warn=${log.counters.warn}`);
1735
+
1736
+ if (log.counters.fail > 0) process.exitCode = 1;
1737
+ }
1738
+
1739
+ function openBrowser(url) {
1740
+ try {
1741
+ execSync(`xdg-open "${url}"`, { stdio: "ignore" });
1742
+ return true;
1743
+ } catch {
1744
+ return false;
1745
+ }
1746
+ }
1747
+
1748
+ function parseJsonBody(req) {
1749
+ return new Promise((resolve, reject) => {
1750
+ let body = "";
1751
+ req.on("data", (chunk) => {
1752
+ body += chunk.toString();
1753
+ if (body.length > 1_000_000) {
1754
+ reject(new Error("request body too large"));
1755
+ }
1756
+ });
1757
+ req.on("end", () => {
1758
+ if (!body) return resolve({});
1759
+ try {
1760
+ resolve(JSON.parse(body));
1761
+ } catch (err) {
1762
+ reject(err);
1763
+ }
1764
+ });
1765
+ req.on("error", reject);
1766
+ });
1767
+ }
1768
+
1769
+ function loadWizardLlmCatalog() {
1770
+ const supportedProviders = new Set([
1771
+ "anthropic",
1772
+ "openai",
1773
+ "openai-codex",
1774
+ "openrouter",
1775
+ "groq",
1776
+ "mistral",
1777
+ "google",
1778
+ "google-vertex",
1779
+ "xai",
1780
+ "deepseek",
1781
+ "together",
1782
+ "perplexity",
1783
+ "amazon-bedrock",
1784
+ "vercel-ai-gateway",
1785
+ "minimax",
1786
+ "moonshot",
1787
+ "nvidia",
1788
+ "qwen",
1789
+ "cerebras",
1790
+ ]);
1791
+ const fallback = {
1792
+ source: "fallback",
1793
+ providers: [
1794
+ {
1795
+ id: "anthropic",
1796
+ models: [{ id: "anthropic/claude-sonnet-4-6", name: "Claude Sonnet 4.6 (recommended default)" }],
1797
+ },
1798
+ {
1799
+ id: "openai",
1800
+ models: [{ id: "openai/gpt-5.4", name: "GPT-5.4" }],
1801
+ },
1802
+ {
1803
+ id: "xai",
1804
+ models: [{ id: "xai/grok-4", name: "Grok 4" }],
1805
+ },
1806
+ {
1807
+ id: "deepseek",
1808
+ models: [{ id: "deepseek/deepseek-chat", name: "DeepSeek Chat (V3.2)" }],
1809
+ },
1810
+ {
1811
+ id: "google",
1812
+ models: [{ id: "google/gemini-2.5-flash", name: "Gemini 2.5 Flash" }],
1813
+ },
1814
+ {
1815
+ id: "groq",
1816
+ models: [{ id: "groq/llama-4-scout-17b-16e-instruct", name: "Llama 4 Scout" }],
1817
+ },
1818
+ {
1819
+ id: "openrouter",
1820
+ models: [{ id: "openrouter/anthropic/claude-sonnet-4-6", name: "Claude Sonnet 4.6 (via OpenRouter)" }],
1821
+ },
1822
+ {
1823
+ id: "mistral",
1824
+ models: [{ id: "mistral/mistral-large-latest", name: "Mistral Large" }],
1825
+ },
1826
+ ],
1827
+ };
1828
+
1829
+ if (!commandExists("openclaw")) {
1830
+ return { ...fallback, warning: "openclaw_not_found" };
1831
+ }
1832
+
1833
+ try {
1834
+ const raw = execSync("openclaw models list --all --json", {
1835
+ encoding: "utf-8",
1836
+ stdio: ["ignore", "pipe", "pipe"],
1837
+ maxBuffer: 50 * 1024 * 1024,
1838
+ timeout: 30_000,
1839
+ env: NO_COLOR_ENV,
1840
+ });
1841
+ const parsed = extractJson(raw);
1842
+ if (!parsed) throw new Error(`Could not extract JSON from openclaw models list output (first 200 chars): ${stripAnsi(raw).slice(0, 200)}`);
1843
+ const models = Array.isArray(parsed?.models) ? parsed.models : [];
1844
+ const providerMap = new Map();
1845
+ for (const entry of models) {
1846
+ if (!entry || typeof entry.key !== "string") continue;
1847
+ const modelId = String(entry.key);
1848
+ const slash = modelId.indexOf("/");
1849
+ if (slash <= 0 || slash === modelId.length - 1) continue;
1850
+ const provider = modelId.slice(0, slash);
1851
+ const existing = providerMap.get(provider) || [];
1852
+ existing.push({
1853
+ id: modelId,
1854
+ name: typeof entry.name === "string" && entry.name.trim() ? entry.name : modelId,
1855
+ });
1856
+ providerMap.set(provider, existing);
1857
+ }
1858
+
1859
+ const providers = [...providerMap.keys()]
1860
+ .sort((a, b) => a.localeCompare(b))
1861
+ .map((id) => {
1862
+ const rawModels = providerMap.get(id) || [];
1863
+ const sortedIds = sortModelsByPreference(
1864
+ id,
1865
+ rawModels.map((m) => m.id),
1866
+ );
1867
+ const byId = new Map(rawModels.map((m) => [m.id, m]));
1868
+ const models = sortedIds.map((mid) => byId.get(mid)).filter(Boolean);
1869
+ return { id, models };
1870
+ })
1871
+ .filter((entry) => supportedProviders.has(entry.id))
1872
+ .filter((entry) => entry.models.length > 0);
1873
+
1874
+ if (providers.length === 0) {
1875
+ return { ...fallback, warning: "openclaw_model_catalog_empty" };
1876
+ }
1877
+
1878
+ return {
1879
+ source: "openclaw",
1880
+ providers,
1881
+ generatedAt: new Date().toISOString(),
1882
+ };
1883
+ } catch (err) {
1884
+ const detail = err?.message || String(err);
1885
+ const isBufferErr = detail.includes("maxBuffer") || detail.includes("ENOBUFS");
1886
+ const hint = isBufferErr
1887
+ ? " (stdout exceeded buffer — OpenClaw model catalog may have grown; this version raises the limit)"
1888
+ : "";
1889
+ console.error(`[traderclaw] loadWizardLlmCatalog failed${hint}: ${detail.slice(0, 500)}`);
1890
+ return {
1891
+ ...fallback,
1892
+ warning: `openclaw_models_list_failed: ${detail}`,
1893
+ };
1894
+ }
1895
+ }
1896
+
1897
+ function wizardHtml(defaults) {
1898
+ return `<!doctype html>
1899
+ <html>
1900
+ <head>
1901
+ <meta charset="utf-8" />
1902
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
1903
+ <title>TraderClaw Installer Wizard</title>
1904
+ <style>
1905
+ body { font-family: ui-sans-serif, system-ui, sans-serif; background:#0b1020; color:#e8eef9; margin:0; }
1906
+ .wrap { max-width: 980px; margin: 24px auto; padding: 0 16px; }
1907
+ .card { background:#121a31; border:1px solid #22315a; border-radius: 12px; padding: 16px; margin-bottom: 16px; }
1908
+ .grid { display:grid; grid-template-columns:1fr 1fr; gap: 12px; }
1909
+ label { display:block; font-size: 12px; color:#9cb0de; margin-bottom: 4px; }
1910
+ input, select { width:100%; padding:10px; border-radius:8px; border:1px solid #334a87; background:#0d1530; color:#e8eef9; }
1911
+ button { border:0; border-radius:8px; padding:10px 14px; background:#4d7cff; color:#fff; cursor:pointer; font-weight:600; }
1912
+ button:disabled { opacity:0.6; cursor:not-allowed; }
1913
+ .muted { color:#9cb0de; font-size:13px; }
1914
+ .ok { color:#78f0a9; }
1915
+ .warn { color:#ffd166; }
1916
+ .err { color:#ff6b6b; }
1917
+ code { background:#0d1530; padding:2px 6px; border-radius:6px; }
1918
+ pre { background:#0d1530; border:1px solid #22315a; border-radius:8px; padding:12px; max-height:300px; overflow:auto; }
1919
+ table { width:100%; border-collapse: collapse; }
1920
+ td, th { border-bottom:1px solid #22315a; padding:8px; font-size:13px; text-align:left; }
1921
+ .cta { background:#0d2a1d; border:1px solid #1f7a47; border-radius:10px; padding:12px; margin-bottom:10px; }
1922
+ .cta h4 { margin:0 0 6px 0; color:#8ef5bc; font-size:14px; }
1923
+ .cta .row { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
1924
+ .cta a, .cta code { color:#b9ffda; word-break:break-all; }
1925
+ .cta button { background:#2f9a5f; padding:8px 10px; font-size:12px; }
1926
+ .cta .important { color:#ffe08a; font-weight:700; margin:8px 0 6px 0; }
1927
+ .checkout { background:#10263f; border:1px solid #2e5785; border-radius:12px; padding:18px; }
1928
+ .checkout h2 { margin:0 0 8px 0; color:#9ee6ff; font-size:24px; }
1929
+ .checkout p { margin:0 0 12px 0; }
1930
+ .checkout-row { display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin-top:8px; }
1931
+ .checkout-row code { flex:1 1 560px; font-size:13px; padding:10px; }
1932
+ .checkout-row button { background:#2d7dff; padding:10px 12px; font-size:13px; }
1933
+ .checkout-finish { margin-top:14px; background:#2f9a5f; font-size:14px; padding:10px 14px; }
1934
+ .hidden { display:none; }
1935
+ .loading-hint { display:flex; align-items:center; gap:8px; margin-top:8px; color:#9cb0de; font-size:13px; }
1936
+ .loading-hint.hidden { display:none; }
1937
+ .spinner { width:14px; height:14px; border:2px solid #334a87; border-top-color:#8daeff; border-radius:50%; animation:spin 0.8s linear infinite; flex:0 0 auto; }
1938
+ .muted a { color:#9fd3ff; }
1939
+ .muted a:hover { color:#c5e5ff; }
1940
+ @keyframes spin { to { transform:rotate(360deg); } }
1941
+ </style>
1942
+ </head>
1943
+ <body>
1944
+ <div class="wrap">
1945
+ <div class="card" id="introCard">
1946
+ <h2>TraderClaw Linux Installer Wizard</h2>
1947
+ <p class="muted">install core services first, then finish wallet setup in your VPS shell.</p>
1948
+ </div>
1949
+ <div class="card" id="llmCard">
1950
+ <h3>Required: OpenClaw LLM Provider</h3>
1951
+ <p class="muted">Pick your LLM provider and paste your credential. Beginner mode supports common API-key providers.</p>
1952
+ <div class="grid">
1953
+ <div>
1954
+ <label>LLM provider (required)</label>
1955
+ <select id="llmProvider"></select>
1956
+ </div>
1957
+ <div>
1958
+ <label>LLM model (advanced, optional)</label>
1959
+ <select id="llmModel"></select>
1960
+ </div>
1961
+ </div>
1962
+ <div style="margin-top:8px;">
1963
+ <label style="display:flex; align-items:center; gap:8px; font-size:13px; color:#9cb0de;">
1964
+ <input id="llmModelManual" type="checkbox" style="width:auto; padding:0; margin:0;" />
1965
+ Choose model manually (advanced)
1966
+ </label>
1967
+ </div>
1968
+ <div style="margin-top:12px;">
1969
+ <label>LLM API key or token (required)</label>
1970
+ <input id="llmCredential" type="password" placeholder="Paste the credential for the selected provider/model" />
1971
+ <p class="muted">This credential is written to OpenClaw model provider config so your agent can run. If you skip manual model selection, the installer will choose a safe provider default.</p>
1972
+ <p class="muted" id="llmLoadState" aria-live="polite">Loading LLM provider catalog...</p>
1973
+ <div id="llmLoadingHint" class="loading-hint" role="status" aria-live="polite">
1974
+ <span class="spinner" aria-hidden="true"></span>
1975
+ <span id="llmLoadingHintText">Fetching provider list...</span>
1976
+ </div>
1977
+ </div>
1978
+ </div>
1979
+ <div class="card" id="xCard">
1980
+ <h3>Optional: X (Twitter) OAuth 1.0a</h3>
1981
+ <p class="muted">Skip this section to use trading and Telegram without X. When set, app keys plus user access token enable the <code>main</code> agent profile (journal &amp; engagement tools).</p>
1982
+ <div class="grid">
1983
+ <div>
1984
+ <label>X consumer key (optional)</label>
1985
+ <input id="xConsumerKey" type="password" autocomplete="off" placeholder="From your X Developer App" />
1986
+ </div>
1987
+ <div>
1988
+ <label>X consumer secret (optional)</label>
1989
+ <input id="xConsumerSecret" type="password" autocomplete="off" />
1990
+ </div>
1991
+ </div>
1992
+ <div class="grid" style="margin-top:12px;">
1993
+ <div>
1994
+ <label>X access token — main profile (optional)</label>
1995
+ <input id="xAccessTokenMain" type="password" autocomplete="off" placeholder="User access token for the posting account" />
1996
+ </div>
1997
+ <div>
1998
+ <label>X access token secret — main profile (optional)</label>
1999
+ <input id="xAccessTokenMainSecret" type="password" autocomplete="off" />
2000
+ </div>
2001
+ </div>
2002
+ <p class="muted">If you use X: create an app at <a href="https://developer.x.com" target="_blank" rel="noopener noreferrer">developer.x.com</a> with OAuth 1.0a Read and Write, and fill all four fields above (or leave all blank). Values are written to <code>openclaw.json</code> under the plugin <code>x</code> block.</p>
2003
+ </div>
2004
+ <div class="card" id="startCard">
2005
+ <div class="grid">
2006
+ <div>
2007
+ <label>Telegram bot token (required)</label>
2008
+ <input id="telegramToken" value="${defaults.telegramToken}" placeholder="Paste your bot token from BotFather" autofocus />
2009
+ <p class="muted">Required for guided onboarding and immediate bot readiness. Need one? <a href="https://core.telegram.org/bots#how-do-i-create-a-bot" target="_blank" rel="noopener noreferrer">Create a Telegram bot token (official docs)</a>.</p>
2010
+ </div>
2011
+ <div>
2012
+ <label>TraderClaw API key (optional for existing users)</label>
2013
+ <input id="apiKey" value="${defaults.apiKey}" placeholder="Leave blank if you are new — setup will create your account" />
2014
+ <p class="muted">Already have a TraderClaw account? Paste your API key here. New users: leave empty.</p>
2015
+ </div>
2016
+ </div>
2017
+ <button id="start" disabled>Start Installation</button>
2018
+ </div>
2019
+ <div class="card" id="statusCard">
2020
+ <h3>Status: <span id="status">idle</span></h3>
2021
+ <p class="muted">Watch progress below. Key links and next actions appear here automatically.</p>
2022
+ <div id="ctaBox" class="hidden">
2023
+ <div id="tailscaleCta" class="cta hidden">
2024
+ <h4>Approve Tailscale</h4>
2025
+ <p class="important">Important: open the link, complete sign-in, then return to this same wizard page.</p>
2026
+ <p class="muted">After you approve Tailscale in the browser, this installer continues from here automatically.</p>
2027
+ <div class="row">
2028
+ <a id="tailscaleLink" href="#" target="_blank" rel="noopener noreferrer"></a>
2029
+ </div>
2030
+ </div>
2031
+ <div id="funnelAdminCta" class="cta hidden">
2032
+ <h4>Enable Tailscale Funnel</h4>
2033
+ <p class="important">Important: open this link in your browser, enable Funnel for this node if prompted, then return to this same wizard page.</p>
2034
+ <p class="muted">The installer continues automatically after Tailscale Funnel is allowed for your tailnet.</p>
2035
+ <div class="row">
2036
+ <a id="funnelAdminLink" href="#" target="_blank" rel="noopener noreferrer"></a>
2037
+ </div>
2038
+ </div>
2039
+ <div id="funnelCta" class="cta hidden">
2040
+ <h4>Gateway public URL (Funnel)</h4>
2041
+ <p class="important">Your gateway may be reachable at the URL below once Funnel is enabled. Keep this page open while installation finishes.</p>
2042
+ <p class="muted">If the URL does not load yet, finish the Tailscale Funnel step above first.</p>
2043
+ <div class="row">
2044
+ <a id="funnelLink" href="#" target="_blank" rel="noopener noreferrer"></a>
2045
+ </div>
2046
+ </div>
2047
+ <div id="setupCta" class="cta hidden">
2048
+ <h4>Final setup will appear when install completes</h4>
2049
+ <p class="muted">When complete, a final checkout screen will show your commands and finish action.</p>
2050
+ </div>
2051
+ </div>
2052
+ <div id="ready" class="ok"></div>
2053
+ <pre id="manual" class="err"></pre>
2054
+ <table>
2055
+ <thead><tr><th>Step</th><th>Status</th><th>Detail</th></tr></thead>
2056
+ <tbody id="steps"></tbody>
2057
+ </table>
2058
+ </div>
2059
+ <div class="card" id="logsCard">
2060
+ <h3>Live Logs</h3>
2061
+ <pre id="logs"></pre>
2062
+ </div>
2063
+ <div class="card checkout hidden" id="completionScreen">
2064
+ <h2>You Made It - Wizard Complete</h2>
2065
+ <p class="muted">TraderClaw core installation is done. Run these 2 commands in your VPS shell to finish setup and go live.</p>
2066
+ <p class="muted">Before trading, continue with the remaining checklist in the install guide: <a href="https://docs.traderclaw.ai/" target="_blank" rel="noopener noreferrer">https://docs.traderclaw.ai/</a></p>
2067
+ <p class="ok" id="setupSuccessText"></p>
2068
+ <div class="checkout-row">
2069
+ <code id="setupCommand"></code>
2070
+ <button id="copySetupCommand" type="button">Copy setup command</button>
2071
+ </div>
2072
+ <div class="checkout-row">
2073
+ <code id="restartCommand"></code>
2074
+ <button id="copyRestartCommand" type="button">Copy restart command</button>
2075
+ </div>
2076
+ <button id="finishWizard" type="button" class="checkout-finish">Finish & Return to Shell</button>
2077
+ </div>
2078
+ </div>
2079
+ <script>
2080
+ const stateEl = document.getElementById("status");
2081
+ const readyEl = document.getElementById("ready");
2082
+ const manualEl = document.getElementById("manual");
2083
+ const stepsEl = document.getElementById("steps");
2084
+ const logsEl = document.getElementById("logs");
2085
+ const ctaBoxEl = document.getElementById("ctaBox");
2086
+ const tailscaleCtaEl = document.getElementById("tailscaleCta");
2087
+ const tailscaleLinkEl = document.getElementById("tailscaleLink");
2088
+ const funnelAdminCtaEl = document.getElementById("funnelAdminCta");
2089
+ const funnelAdminLinkEl = document.getElementById("funnelAdminLink");
2090
+ const funnelCtaEl = document.getElementById("funnelCta");
2091
+ const funnelLinkEl = document.getElementById("funnelLink");
2092
+ const setupCtaEl = document.getElementById("setupCta");
2093
+ const setupSuccessTextEl = document.getElementById("setupSuccessText");
2094
+ const setupCommandEl = document.getElementById("setupCommand");
2095
+ const restartCommandEl = document.getElementById("restartCommand");
2096
+ const copySetupBtn = document.getElementById("copySetupCommand");
2097
+ const copyRestartBtn = document.getElementById("copyRestartCommand");
2098
+ const finishWizardBtn = document.getElementById("finishWizard");
2099
+ const llmProviderEl = document.getElementById("llmProvider");
2100
+ const llmModelEl = document.getElementById("llmModel");
2101
+ const llmModelManualEl = document.getElementById("llmModelManual");
2102
+ const llmCredentialEl = document.getElementById("llmCredential");
2103
+ const telegramTokenEl = document.getElementById("telegramToken");
2104
+ const llmLoadStateEl = document.getElementById("llmLoadState");
2105
+ const llmLoadingHintEl = document.getElementById("llmLoadingHint");
2106
+ const llmLoadingHintTextEl = document.getElementById("llmLoadingHintText");
2107
+ const startBtn = document.getElementById("start");
2108
+ const llmCardEl = document.getElementById("llmCard");
2109
+ const xCardEl = document.getElementById("xCard");
2110
+ const xConsumerKeyEl = document.getElementById("xConsumerKey");
2111
+ const xConsumerSecretEl = document.getElementById("xConsumerSecret");
2112
+ const xAccessTokenMainEl = document.getElementById("xAccessTokenMain");
2113
+ const xAccessTokenMainSecretEl = document.getElementById("xAccessTokenMainSecret");
2114
+ const startCardEl = document.getElementById("startCard");
2115
+ const statusCardEl = document.getElementById("statusCard");
2116
+ const logsCardEl = document.getElementById("logsCard");
2117
+ const completionScreenEl = document.getElementById("completionScreen");
2118
+ let llmCatalog = { providers: [] };
2119
+ let llmCatalogReady = false;
2120
+ let llmCatalogLoading = false;
2121
+ let llmLoadTicker = null;
2122
+ let llmLoadStartedAt = 0;
2123
+ let announcedTailscaleUrl = "";
2124
+ let announcedFunnelAdminUrl = "";
2125
+ let pollTimer = null;
2126
+ let pollIntervalMs = 1200;
2127
+ let installLocked = false;
2128
+
2129
+ function hasRequiredInputs() {
2130
+ return (
2131
+ llmCatalogReady
2132
+ && Boolean(llmProviderEl.value.trim())
2133
+ && Boolean(llmCredentialEl.value.trim())
2134
+ && Boolean(telegramTokenEl.value.trim())
2135
+ );
2136
+ }
2137
+
2138
+ /** All-or-nothing: 0 or 4 non-empty X fields; partial is invalid. */
2139
+ function xWizardFieldsStatus() {
2140
+ const fields = [
2141
+ xConsumerKeyEl.value.trim(),
2142
+ xConsumerSecretEl.value.trim(),
2143
+ xAccessTokenMainEl.value.trim(),
2144
+ xAccessTokenMainSecretEl.value.trim(),
2145
+ ];
2146
+ const filled = fields.filter(Boolean).length;
2147
+ return { filled, total: 4, ok: filled === 0 || filled === 4 };
2148
+ }
2149
+
2150
+ function updateStartButtonState() {
2151
+ if (installLocked) {
2152
+ startBtn.disabled = true;
2153
+ startBtn.setAttribute("aria-busy", "true");
2154
+ if (!llmCatalogLoading) {
2155
+ startBtn.textContent = "Installation in progress…";
2156
+ }
2157
+ return;
2158
+ }
2159
+ startBtn.removeAttribute("aria-busy");
2160
+ startBtn.disabled = llmCatalogLoading || !hasRequiredInputs() || !xWizardFieldsStatus().ok;
2161
+ if (!llmCatalogLoading) {
2162
+ startBtn.textContent = "Start Installation";
2163
+ }
2164
+ }
2165
+
2166
+ function stopLlmLoadTicker() {
2167
+ if (llmLoadTicker) {
2168
+ clearInterval(llmLoadTicker);
2169
+ llmLoadTicker = null;
2170
+ }
2171
+ }
2172
+
2173
+ function setLlmCatalogLoading(loading) {
2174
+ llmCatalogLoading = loading;
2175
+ llmProviderEl.disabled = loading;
2176
+ llmModelManualEl.disabled = loading;
2177
+ llmModelEl.disabled = loading || !llmModelManualEl.checked;
2178
+ if (loading) {
2179
+ llmLoadStartedAt = Date.now();
2180
+ llmLoadingHintEl.classList.remove("hidden");
2181
+ startBtn.textContent = "Loading providers...";
2182
+ const updateHint = () => {
2183
+ const elapsedSeconds = Math.max(1, Math.floor((Date.now() - llmLoadStartedAt) / 1000));
2184
+ if (elapsedSeconds >= 8) {
2185
+ llmLoadingHintTextEl.textContent = "Still loading provider catalog (" + elapsedSeconds + "s). First run can take up to ~20s.";
2186
+ return;
2187
+ }
2188
+ llmLoadingHintTextEl.textContent = "Fetching provider list (" + elapsedSeconds + "s)...";
2189
+ };
2190
+ updateHint();
2191
+ stopLlmLoadTicker();
2192
+ llmLoadTicker = setInterval(updateHint, 1000);
2193
+ updateStartButtonState();
2194
+ return;
2195
+ }
2196
+ stopLlmLoadTicker();
2197
+ llmLoadingHintEl.classList.add("hidden");
2198
+ llmLoadStartedAt = 0;
2199
+ llmModelEl.disabled = !llmModelManualEl.checked;
2200
+ updateStartButtonState();
2201
+ }
2202
+
2203
+ function setLlmCatalogReady(ready, message, isError = false) {
2204
+ llmCatalogReady = ready;
2205
+ llmLoadStateEl.textContent = message;
2206
+ llmLoadStateEl.className = isError ? "err" : "muted";
2207
+ updateStartButtonState();
2208
+ }
2209
+
2210
+ function setSelectOptions(selectEl, items, value) {
2211
+ selectEl.innerHTML = "";
2212
+ items.forEach((item) => {
2213
+ const option = document.createElement("option");
2214
+ option.value = item.value;
2215
+ option.textContent = item.label;
2216
+ selectEl.appendChild(option);
2217
+ });
2218
+ if (value) selectEl.value = value;
2219
+ }
2220
+
2221
+ function refreshModelOptions(preferredModel) {
2222
+ const provider = llmProviderEl.value;
2223
+ const providerEntry = (llmCatalog.providers || []).find((entry) => entry.id === provider);
2224
+ const modelItems = (providerEntry ? providerEntry.models : []).map((item) => ({ value: item.id, label: item.name + " (" + item.id + ")" }));
2225
+ if (modelItems.length === 0) {
2226
+ setSelectOptions(llmModelEl, [{ value: "", label: "No models available for provider" }], "");
2227
+ updateStartButtonState();
2228
+ return;
2229
+ }
2230
+ setSelectOptions(llmModelEl, modelItems, preferredModel || modelItems[0].value);
2231
+ llmModelEl.disabled = !llmModelManualEl.checked;
2232
+ updateStartButtonState();
2233
+ }
2234
+
2235
+ async function loadLlmCatalog() {
2236
+ setLlmCatalogLoading(true);
2237
+ setSelectOptions(llmProviderEl, [{ value: "", label: "Loading providers..." }], "");
2238
+ setSelectOptions(llmModelEl, [{ value: "", label: "Loading models..." }], "");
2239
+ setLlmCatalogReady(false, "Loading LLM provider catalog... this can take a few seconds.");
2240
+ try {
2241
+ const res = await fetch("/api/llm/options");
2242
+ const data = await res.json();
2243
+ llmCatalog = data || { providers: [] };
2244
+ const providers = (llmCatalog.providers || []).map((entry) => ({ value: entry.id, label: entry.id }));
2245
+ if (providers.length === 0) {
2246
+ setSelectOptions(llmProviderEl, [{ value: "", label: "No providers available" }], "");
2247
+ refreshModelOptions("");
2248
+ setLlmCatalogReady(false, "No LLM providers were found from OpenClaw. Please check OpenClaw model setup.", true);
2249
+ return;
2250
+ }
2251
+ setSelectOptions(llmProviderEl, providers, "${defaults.llmProvider}");
2252
+ refreshModelOptions("${defaults.llmModel}");
2253
+ const isFallback = llmCatalog.source === "fallback";
2254
+ const catalogMsg = isFallback
2255
+ ? "Showing safe defaults only (could not load full OpenClaw catalog" + (llmCatalog.warning ? ": " + llmCatalog.warning : "") + "). These providers still work — pick one and paste your credential."
2256
+ : "LLM providers loaded. Select provider and paste credential to continue. Model selection is optional.";
2257
+ setLlmCatalogReady(true, catalogMsg, isFallback);
2258
+ } catch (err) {
2259
+ setLlmCatalogReady(false, "Failed to load LLM providers. Check OpenClaw and reload this page.", true);
2260
+ manualEl.textContent = "Failed to load LLM provider catalog: " + (err && err.message ? err.message : String(err));
2261
+ } finally {
2262
+ setLlmCatalogLoading(false);
2263
+ }
2264
+ }
2265
+
2266
+ function showUrlCta(containerEl, linkEl, value) {
2267
+ if (!value) {
2268
+ containerEl.classList.add("hidden");
2269
+ linkEl.textContent = "";
2270
+ linkEl.removeAttribute("href");
2271
+ return;
2272
+ }
2273
+ ctaBoxEl.classList.remove("hidden");
2274
+ containerEl.classList.remove("hidden");
2275
+ linkEl.href = value;
2276
+ linkEl.textContent = value;
2277
+ }
2278
+
2279
+ function setCheckoutMode(enabled) {
2280
+ if (enabled) {
2281
+ llmCardEl.classList.add("hidden");
2282
+ xCardEl.classList.add("hidden");
2283
+ startCardEl.classList.add("hidden");
2284
+ statusCardEl.classList.add("hidden");
2285
+ logsCardEl.classList.add("hidden");
2286
+ completionScreenEl.classList.remove("hidden");
2287
+ return;
2288
+ }
2289
+ llmCardEl.classList.remove("hidden");
2290
+ xCardEl.classList.remove("hidden");
2291
+ startCardEl.classList.remove("hidden");
2292
+ statusCardEl.classList.remove("hidden");
2293
+ logsCardEl.classList.remove("hidden");
2294
+ completionScreenEl.classList.add("hidden");
2295
+ }
2296
+
2297
+ async function startInstall() {
2298
+ if (!llmCatalogReady) {
2299
+ stateEl.textContent = "blocked";
2300
+ readyEl.textContent = "";
2301
+ manualEl.textContent = llmCatalogLoading
2302
+ ? "LLM provider catalog is still loading. Wait until the loading indicator finishes."
2303
+ : "LLM provider catalog is not ready yet. Reload the page and try again.";
2304
+ return;
2305
+ }
2306
+ stateEl.textContent = "starting";
2307
+ manualEl.textContent = "";
2308
+ readyEl.textContent = "Starting installation...";
2309
+
2310
+ const payload = {
2311
+ llmProvider: llmProviderEl.value.trim(),
2312
+ llmModel: llmModelManualEl.checked ? llmModelEl.value.trim() : "",
2313
+ llmCredential: llmCredentialEl.value.trim(),
2314
+ apiKey: document.getElementById("apiKey").value.trim(),
2315
+ telegramToken: document.getElementById("telegramToken").value.trim(),
2316
+ xConsumerKey: xConsumerKeyEl.value.trim(),
2317
+ xConsumerSecret: xConsumerSecretEl.value.trim(),
2318
+ xAccessTokenMain: xAccessTokenMainEl.value.trim(),
2319
+ xAccessTokenMainSecret: xAccessTokenMainSecretEl.value.trim(),
2320
+ };
2321
+ if (!payload.llmProvider || !payload.llmCredential) {
2322
+ stateEl.textContent = "blocked";
2323
+ readyEl.textContent = "";
2324
+ manualEl.textContent = "LLM provider and credential are required before starting installation.";
2325
+ return;
2326
+ }
2327
+ if (!payload.telegramToken) {
2328
+ stateEl.textContent = "blocked";
2329
+ readyEl.textContent = "";
2330
+ manualEl.textContent = "Telegram bot token is required before starting installation.";
2331
+ return;
2332
+ }
2333
+ const xStatus = xWizardFieldsStatus();
2334
+ if (!xStatus.ok) {
2335
+ stateEl.textContent = "blocked";
2336
+ readyEl.textContent = "";
2337
+ manualEl.textContent =
2338
+ "X (Twitter) credentials are optional: leave all four fields blank, or fill consumer key, consumer secret, access token, and access token secret.";
2339
+ return;
2340
+ }
2341
+
2342
+ installLocked = true;
2343
+ updateStartButtonState();
2344
+
2345
+ try {
2346
+ const res = await fetch("/api/start", {
2347
+ method: "POST",
2348
+ headers: { "content-type": "application/json" },
2349
+ body: JSON.stringify(payload),
2350
+ });
2351
+ const data = await res.json().catch(() => ({}));
2352
+
2353
+ if (!res.ok) {
2354
+ installLocked = false;
2355
+ updateStartButtonState();
2356
+ stateEl.textContent = "failed";
2357
+ manualEl.textContent = data.error ? "Failed to start: " + data.error : "Failed to start installation.";
2358
+ readyEl.textContent = "";
2359
+ return;
2360
+ }
2361
+
2362
+ readyEl.textContent = "Installation started. Live progress will appear below.";
2363
+ announcedTailscaleUrl = "";
2364
+ announcedFunnelAdminUrl = "";
2365
+ await refresh();
2366
+ } catch (err) {
2367
+ installLocked = false;
2368
+ updateStartButtonState();
2369
+ stateEl.textContent = "failed";
2370
+ manualEl.textContent = "Failed to start installation: " + (err && err.message ? err.message : String(err));
2371
+ readyEl.textContent = "";
2372
+ }
2373
+ }
2374
+
2375
+ function setPollInterval(ms) {
2376
+ if (ms === pollIntervalMs && pollTimer) return;
2377
+ pollIntervalMs = ms;
2378
+ if (pollTimer) clearInterval(pollTimer);
2379
+ pollTimer = setInterval(refresh, pollIntervalMs);
2380
+ }
2381
+
2382
+ async function refresh() {
2383
+ const res = await fetch("/api/state");
2384
+ const data = await res.json();
2385
+ stateEl.textContent = data.status || "idle";
2386
+ const st = data.status || "idle";
2387
+ installLocked = st === "running" || st === "completed";
2388
+
2389
+ const steps = data.stepResults || [];
2390
+ const stepDone = (id) => steps.some((r) => r.stepId === id && r.status === "completed");
2391
+ const tailscaleUpDone = stepDone("tailscale_up");
2392
+ const gatewayBootstrapDone = stepDone("gateway_bootstrap");
2393
+
2394
+ const tailscaleApprovalUrl = data.detected && data.detected.tailscaleApprovalUrl ? data.detected.tailscaleApprovalUrl : "";
2395
+ const funnelUrl = data.detected && data.detected.funnelUrl ? data.detected.funnelUrl : "";
2396
+ const funnelAdminUrl = data.detected && data.detected.funnelAdminUrl ? data.detected.funnelAdminUrl : "";
2397
+
2398
+ const showTailscaleCta = tailscaleApprovalUrl && !tailscaleUpDone;
2399
+ showUrlCta(tailscaleCtaEl, tailscaleLinkEl, showTailscaleCta ? tailscaleApprovalUrl : "");
2400
+
2401
+ const showFunnelAdmin = funnelAdminUrl && !gatewayBootstrapDone;
2402
+ showUrlCta(funnelAdminCtaEl, funnelAdminLinkEl, showFunnelAdmin ? funnelAdminUrl : "");
2403
+
2404
+ const showFunnelPublic = funnelUrl && !gatewayBootstrapDone;
2405
+ showUrlCta(funnelCtaEl, funnelLinkEl, showFunnelPublic ? funnelUrl : "");
2406
+
2407
+ if (showTailscaleCta && tailscaleApprovalUrl !== announcedTailscaleUrl) {
2408
+ announcedTailscaleUrl = tailscaleApprovalUrl;
2409
+ try {
2410
+ window.alert("Action required: approve Tailscale in your browser. The approval link is now shown above status.");
2411
+ } catch {
2412
+ // Some environments can suppress alerts; CTA remains visible.
2413
+ }
2414
+ }
2415
+ if (showFunnelAdmin && funnelAdminUrl !== announcedFunnelAdminUrl) {
2416
+ announcedFunnelAdminUrl = funnelAdminUrl;
2417
+ try {
2418
+ window.alert("Action required: enable Tailscale Funnel in your browser. The link is shown above the status table.");
2419
+ } catch {
2420
+ // ignore
2421
+ }
2422
+ }
2423
+
2424
+ const setupHandoff = data.setupHandoff;
2425
+ if (data.status === "completed" && setupHandoff && setupHandoff.command) {
2426
+ setPollInterval(1200);
2427
+ setCheckoutMode(true);
2428
+ ctaBoxEl.classList.remove("hidden");
2429
+ setupCtaEl.classList.remove("hidden");
2430
+ setupSuccessTextEl.textContent = "Pro move. You finished the wizard installation.";
2431
+ setupCommandEl.textContent = setupHandoff.command;
2432
+ restartCommandEl.textContent = setupHandoff.restartCommand || "openclaw gateway restart";
2433
+ readyEl.textContent =
2434
+ setupHandoff.title + "\\n" +
2435
+ setupHandoff.message + "\\n" +
2436
+ "Run in VPS shell: " + setupHandoff.command + "\\n" +
2437
+ "Then run: " + (setupHandoff.restartCommand || "openclaw gateway restart");
2438
+ } else {
2439
+ setCheckoutMode(false);
2440
+ setupCtaEl.classList.add("hidden");
2441
+ const anyCta =
2442
+ showTailscaleCta
2443
+ || showFunnelAdmin
2444
+ || showFunnelPublic;
2445
+ if (anyCta) {
2446
+ ctaBoxEl.classList.remove("hidden");
2447
+ } else {
2448
+ ctaBoxEl.classList.add("hidden");
2449
+ }
2450
+
2451
+ if (data.status === "running") {
2452
+ setPollInterval(500);
2453
+ if (showTailscaleCta) {
2454
+ readyEl.textContent =
2455
+ "Action required: open the Tailscale approval link and complete sign-in, then return to this page.";
2456
+ } else if (showFunnelAdmin) {
2457
+ readyEl.textContent =
2458
+ "Action required: open the Tailscale Funnel link above and complete any prompts, then return here.";
2459
+ } else if (showFunnelPublic) {
2460
+ readyEl.textContent =
2461
+ "Public gateway URL is shown above — installation continues. Please keep this page open.";
2462
+ } else if (tailscaleUpDone && !gatewayBootstrapDone) {
2463
+ readyEl.textContent =
2464
+ "Tailscale is connected — installation is continuing. Please keep this page open and be patient.";
2465
+ } else {
2466
+ readyEl.textContent = "Installation running — please wait…";
2467
+ }
2468
+ } else {
2469
+ setPollInterval(1200);
2470
+ if (data.status !== "completed") {
2471
+ readyEl.textContent = "";
2472
+ }
2473
+ }
2474
+ }
2475
+
2476
+ const errors = data.errors || [];
2477
+ manualEl.textContent = errors.length > 0
2478
+ ? errors.map((e) => "Step " + (e.stepId || "unknown") + ":\\n" + (e.error || "")).join("\\n\\n")
2479
+ : "";
2480
+ stepsEl.innerHTML = "";
2481
+ steps.forEach((row) => {
2482
+ const tr = document.createElement("tr");
2483
+ tr.innerHTML = "<td>" + row.stepId + "</td><td>" + row.status + "</td><td>" + (row.error || row.detail || "") + "</td>";
2484
+ stepsEl.appendChild(tr);
2485
+ });
2486
+ logsEl.textContent = (data.logs || []).map((l) => "[" + l.at + "] " + l.stepId + " " + l.level + " " + l.text).join("\\n");
2487
+ updateStartButtonState();
2488
+ }
2489
+
2490
+ async function finishWizardServer() {
2491
+ finishWizardBtn.disabled = true;
2492
+ finishWizardBtn.textContent = "Closing wizard...";
2493
+ try {
2494
+ const res = await fetch("/api/finish", { method: "POST" });
2495
+ const data = await res.json().catch(() => ({}));
2496
+ if (!res.ok) {
2497
+ finishWizardBtn.disabled = false;
2498
+ finishWizardBtn.textContent = "Finish & Return to Shell";
2499
+ manualEl.textContent = data.error ? "Unable to close wizard: " + data.error : "Unable to close wizard right now.";
2500
+ return;
2501
+ }
2502
+ finishWizardBtn.textContent = "Finished - shell is ready";
2503
+ readyEl.textContent = "Wizard completed. Server is shutting down and your shell prompt should already be back.";
2504
+ manualEl.textContent = "";
2505
+ } catch (err) {
2506
+ const msg = err && err.message ? err.message : String(err);
2507
+ const likelyClosed = /failed to fetch|networkerror|network request failed/i.test(msg);
2508
+ if (likelyClosed) {
2509
+ // Server can close immediately after acknowledging /api/finish, which may race the browser fetch.
2510
+ finishWizardBtn.textContent = "Finished - shell is ready";
2511
+ readyEl.textContent = "Wizard finish was requested. Your shell prompt should be back.";
2512
+ manualEl.textContent = "";
2513
+ return;
2514
+ }
2515
+ finishWizardBtn.disabled = false;
2516
+ finishWizardBtn.textContent = "Finish & Return to Shell";
2517
+ manualEl.textContent = "Unable to close wizard: " + msg;
2518
+ }
2519
+ }
2520
+
2521
+ document.getElementById("start").addEventListener("click", startInstall);
2522
+ async function copyWithFeedback(buttonEl, value) {
2523
+ if (!value) return;
2524
+ try {
2525
+ await navigator.clipboard.writeText(value);
2526
+ buttonEl.textContent = "Copied";
2527
+ } catch {
2528
+ buttonEl.textContent = "Copy failed";
2529
+ }
2530
+ setTimeout(() => { buttonEl.textContent = "Copy command"; }, 1200);
2531
+ }
2532
+
2533
+ copySetupBtn.addEventListener("click", async () => {
2534
+ await copyWithFeedback(copySetupBtn, setupCommandEl.textContent || "");
2535
+ });
2536
+ copyRestartBtn.addEventListener("click", async () => {
2537
+ await copyWithFeedback(copyRestartBtn, restartCommandEl.textContent || "");
2538
+ });
2539
+ finishWizardBtn.addEventListener("click", finishWizardServer);
2540
+ llmProviderEl.addEventListener("change", () => refreshModelOptions(""));
2541
+ llmModelManualEl.addEventListener("change", () => {
2542
+ llmModelEl.disabled = !llmModelManualEl.checked;
2543
+ updateStartButtonState();
2544
+ });
2545
+ llmCredentialEl.addEventListener("input", updateStartButtonState);
2546
+ telegramTokenEl.addEventListener("input", updateStartButtonState);
2547
+ xConsumerKeyEl.addEventListener("input", updateStartButtonState);
2548
+ xConsumerSecretEl.addEventListener("input", updateStartButtonState);
2549
+ xAccessTokenMainEl.addEventListener("input", updateStartButtonState);
2550
+ xAccessTokenMainSecretEl.addEventListener("input", updateStartButtonState);
2551
+ loadLlmCatalog();
2552
+ setPollInterval(1200);
2553
+ refresh();
2554
+ </script>
2555
+ </body>
2556
+ </html>`;
2557
+ }
2558
+
2559
+ async function cmdInstall(args) {
2560
+ const wizard = args.includes("--wizard");
2561
+ if (!wizard) {
2562
+ printError("Only wizard mode is currently supported. Use: traderclaw install --wizard");
2563
+ process.exit(1);
2564
+ }
2565
+
2566
+ const defaults = parseInstallWizardArgs(args);
2567
+ const { createInstallerStepEngine, assertWizardXCredentials } = await import("./installer-step-engine.mjs");
2568
+ const modeConfig = {
2569
+ pluginPackage: "solana-traderclaw",
2570
+ pluginId: "solana-trader",
2571
+ cliName: "traderclaw",
2572
+ gatewayConfig: "gateway-v1.json5",
2573
+ agents: ["cto", "onchain-analyst", "alpha-signal-analyst", "risk-officer", "strategy-researcher"],
2574
+ };
2575
+
2576
+ const runtime = {
2577
+ status: "idle",
2578
+ logs: [],
2579
+ stepResults: [],
2580
+ detected: { funnelUrl: null, tailscaleApprovalUrl: null, funnelAdminUrl: null },
2581
+ errors: [],
2582
+ setupHandoff: null,
2583
+ };
2584
+ let running = false;
2585
+ let shuttingDown = false;
2586
+
2587
+ const server = createServer(async (req, res) => {
2588
+ const respondJson = (code, payload) => {
2589
+ res.statusCode = code;
2590
+ res.setHeader("content-type", "application/json");
2591
+ res.end(JSON.stringify(payload));
2592
+ };
2593
+ const extractTailscaleApprovalUrl = (evt) => {
2594
+ const urls = Array.isArray(evt?.urls) ? evt.urls : [];
2595
+ for (const url of urls) {
2596
+ if (typeof url === "string" && url.startsWith("https://login.tailscale.com/")) return url;
2597
+ }
2598
+ const text = typeof evt?.text === "string" ? evt.text : "";
2599
+ const match = text.match(/https:\/\/login\.tailscale\.com\/[^\s"')]+/);
2600
+ return match ? match[0] : "";
2601
+ };
2602
+
2603
+ const extractFunnelAdminUrlFromText = (text) => {
2604
+ const t = typeof text === "string" ? text : "";
2605
+ const m = t.match(/https:\/\/login\.tailscale\.com\/f\/funnel[^\s"'`)]+/);
2606
+ return m ? m[0] : "";
2607
+ };
2608
+
2609
+ const extractPublicGatewayUrlFromText = (text) => {
2610
+ const t = typeof text === "string" ? text : "";
2611
+ const matches = t.match(/https?:\/\/[^\s"'`)]+/g) || [];
2612
+ for (const u of matches) {
2613
+ if (u.includes("login.tailscale.com")) continue;
2614
+ if (u.includes("ts.net") || u.includes("trycloudflare.com")) return u;
2615
+ }
2616
+ return "";
2617
+ };
2618
+
2619
+ if (req.method === "GET" && req.url === "/") {
2620
+ res.statusCode = 200;
2621
+ res.setHeader("content-type", "text/html; charset=utf-8");
2622
+ res.end(wizardHtml(defaults));
2623
+ return;
2624
+ }
2625
+
2626
+ if (req.method === "GET" && req.url === "/api/state") {
2627
+ respondJson(200, runtime);
2628
+ return;
2629
+ }
2630
+
2631
+ if (req.method === "GET" && req.url === "/api/llm/options") {
2632
+ respondJson(200, loadWizardLlmCatalog());
2633
+ return;
2634
+ }
2635
+
2636
+ if (req.method === "POST" && req.url === "/api/finish") {
2637
+ if (running || runtime.status !== "completed") {
2638
+ respondJson(409, { ok: false, error: "wizard_not_completed" });
2639
+ return;
2640
+ }
2641
+ if (shuttingDown) {
2642
+ respondJson(202, { ok: true, shuttingDown: true });
2643
+ return;
2644
+ }
2645
+ shuttingDown = true;
2646
+ respondJson(202, { ok: true, shuttingDown: true });
2647
+ setTimeout(() => {
2648
+ const setupCommand = runtime.setupHandoff?.command || "";
2649
+ const restartCommand = runtime.setupHandoff?.restartCommand || "openclaw gateway restart";
2650
+ if (setupCommand) {
2651
+ printSuccess("Run these commands now in this same terminal:");
2652
+ print(` 1) ${setupCommand}`);
2653
+ print(` 2) ${restartCommand}`);
2654
+ } else {
2655
+ printWarn("Wizard finished, but no setup handoff command was available.");
2656
+ print(` Next step: traderclaw setup --url ${defaults.orchestratorUrl}`);
2657
+ print(` Then run: ${restartCommand}`);
2658
+ }
2659
+ printInfo("Wizard finish requested from browser. Closing server and returning shell prompt.");
2660
+ server.close(() => process.exit(0));
2661
+ }, 650);
2662
+ return;
2663
+ }
2664
+
2665
+ if (req.method === "POST" && req.url === "/api/start") {
2666
+ if (running) {
2667
+ respondJson(409, { ok: false, error: "wizard_run_already_in_progress" });
2668
+ return;
2669
+ }
2670
+
2671
+ const body = await parseJsonBody(req).catch(() => ({}));
2672
+ const wizardOpts = {
2673
+ mode: "light",
2674
+ lane: defaults.lane,
2675
+ llmProvider: body.llmProvider || defaults.llmProvider,
2676
+ llmModel: body.llmModel || defaults.llmModel,
2677
+ llmCredential: body.llmCredential || defaults.llmCredential,
2678
+ apiKey: body.apiKey || defaults.apiKey,
2679
+ orchestratorUrl: defaults.orchestratorUrl,
2680
+ gatewayBaseUrl: defaults.gatewayBaseUrl,
2681
+ gatewayToken: defaults.gatewayToken,
2682
+ enableTelegram: true,
2683
+ telegramToken: body.telegramToken || defaults.telegramToken,
2684
+ autoInstallDeps: true,
2685
+ xConsumerKey: body.xConsumerKey ?? defaults.xConsumerKey,
2686
+ xConsumerSecret: body.xConsumerSecret ?? defaults.xConsumerSecret,
2687
+ xAccessTokenMain: body.xAccessTokenMain ?? defaults.xAccessTokenMain,
2688
+ xAccessTokenMainSecret: body.xAccessTokenMainSecret ?? defaults.xAccessTokenMainSecret,
2689
+ };
2690
+ const xErr = assertWizardXCredentials(modeConfig, wizardOpts);
2691
+ if (xErr) {
2692
+ respondJson(400, { ok: false, error: xErr });
2693
+ return;
2694
+ }
2695
+
2696
+ running = true;
2697
+ runtime.status = "running";
2698
+ runtime.logs = [];
2699
+ runtime.stepResults = [];
2700
+ runtime.errors = [];
2701
+ runtime.detected = { funnelUrl: null, tailscaleApprovalUrl: null, funnelAdminUrl: null };
2702
+ runtime.setupHandoff = null;
2703
+ respondJson(202, { ok: true });
2704
+
2705
+ const engine = createInstallerStepEngine(
2706
+ modeConfig,
2707
+ {
2708
+ mode: "light",
2709
+ lane: defaults.lane,
2710
+ llmProvider: body.llmProvider || defaults.llmProvider,
2711
+ llmModel: body.llmModel || defaults.llmModel,
2712
+ llmCredential: body.llmCredential || defaults.llmCredential,
2713
+ apiKey: body.apiKey || defaults.apiKey,
2714
+ orchestratorUrl: defaults.orchestratorUrl,
2715
+ gatewayBaseUrl: defaults.gatewayBaseUrl,
2716
+ gatewayToken: defaults.gatewayToken,
2717
+ enableTelegram: true,
2718
+ telegramToken: body.telegramToken || defaults.telegramToken,
2719
+ autoInstallDeps: true,
2720
+ xConsumerKey: wizardOpts.xConsumerKey,
2721
+ xConsumerSecret: wizardOpts.xConsumerSecret,
2722
+ xAccessTokenMain: wizardOpts.xAccessTokenMain,
2723
+ xAccessTokenMainSecret: wizardOpts.xAccessTokenMainSecret,
2724
+ },
2725
+ {
2726
+ onStepEvent: (evt) => {
2727
+ const existing = runtime.stepResults.find((s) => s.stepId === evt.stepId);
2728
+ if (!existing) runtime.stepResults.push({ stepId: evt.stepId, status: evt.status, detail: evt.detail || "" });
2729
+ else {
2730
+ existing.status = evt.status;
2731
+ existing.detail = evt.detail || existing.detail;
2732
+ }
2733
+ },
2734
+ onLog: (evt) => {
2735
+ runtime.logs.push(evt);
2736
+ const stepId = evt.stepId || "";
2737
+ const text = typeof evt.text === "string" ? evt.text : "";
2738
+ if (!runtime.detected.tailscaleApprovalUrl && stepId === "tailscale_up") {
2739
+ const approvalUrl = extractTailscaleApprovalUrl(evt);
2740
+ if (approvalUrl) runtime.detected.tailscaleApprovalUrl = approvalUrl;
2741
+ }
2742
+ if (stepId === "funnel" || stepId === "gateway_bootstrap") {
2743
+ const urls = Array.isArray(evt.urls) ? evt.urls : [];
2744
+ for (const u of urls) {
2745
+ if (typeof u === "string" && u.includes("login.tailscale.com/f/funnel")) {
2746
+ runtime.detected.funnelAdminUrl = u;
2747
+ }
2748
+ }
2749
+ const adminFromText = extractFunnelAdminUrlFromText(text);
2750
+ if (adminFromText) runtime.detected.funnelAdminUrl = adminFromText;
2751
+ const pub = extractPublicGatewayUrlFromText(text);
2752
+ if (pub) runtime.detected.funnelUrl = pub;
2753
+ }
2754
+ },
2755
+ },
2756
+ );
2757
+
2758
+ const result = await engine.runAll();
2759
+ runtime.status = result.status;
2760
+ runtime.stepResults = result.stepResults || runtime.stepResults;
2761
+ const mergedDetected = result.detected && typeof result.detected === "object" ? result.detected : {};
2762
+ runtime.detected = {
2763
+ tailscaleApprovalUrl: mergedDetected.tailscaleApprovalUrl ?? runtime.detected.tailscaleApprovalUrl,
2764
+ funnelUrl: mergedDetected.funnelUrl ?? runtime.detected.funnelUrl,
2765
+ funnelAdminUrl: runtime.detected.funnelAdminUrl ?? mergedDetected.funnelAdminUrl ?? null,
2766
+ };
2767
+ runtime.errors = result.errors || [];
2768
+ runtime.setupHandoff = result.setupHandoff || null;
2769
+ running = false;
2770
+ return;
2771
+ }
2772
+
2773
+ respondJson(404, { ok: false, error: "not_found" });
2774
+ });
2775
+
2776
+ await new Promise((resolve, reject) => {
2777
+ server.once("error", reject);
2778
+ server.listen(defaults.port, "127.0.0.1", resolve);
2779
+ });
2780
+
2781
+ const url = `http://127.0.0.1:${defaults.port}`;
2782
+ printSuccess(`Installer wizard is running at ${url}`);
2783
+ if (!openBrowser(url)) {
2784
+ printInfo(`Open this URL in your browser: ${url}`);
2785
+ }
2786
+ printInfo("Press Ctrl+C to stop the wizard server.");
2787
+ }
2788
+
2789
+ async function cmdTestSession(args) {
2790
+ const config = readConfig();
2791
+ const pluginConfig = getPluginConfig(config);
2792
+
2793
+ if (!pluginConfig) {
2794
+ printError("No plugin configuration found. Run 'traderclaw setup' first.");
2795
+ process.exit(1);
2796
+ }
2797
+
2798
+ const orchestratorUrl = pluginConfig.orchestratorUrl;
2799
+ if (!orchestratorUrl) {
2800
+ printError("orchestratorUrl not set in config. Run 'traderclaw setup' to fix.");
2801
+ process.exit(1);
2802
+ }
2803
+
2804
+ const dataDir = pluginConfig.dataDir || join(process.cwd(), ".traderclaw-v1-data");
2805
+ const sessionTokensPath = join(dataDir, "session-tokens.json");
2806
+
2807
+ let sidecar = null;
2808
+ try {
2809
+ if (existsSync(sessionTokensPath)) {
2810
+ sidecar = JSON.parse(readFileSync(sessionTokensPath, "utf-8"));
2811
+ }
2812
+ } catch { /* ignore */ }
2813
+
2814
+ const effectiveRefreshToken =
2815
+ (sidecar?.refreshToken && sidecar.refreshToken.length > 0)
2816
+ ? sidecar.refreshToken
2817
+ : pluginConfig.refreshToken;
2818
+
2819
+ const walletPrivateKeyInput = args.includes("--wallet-private-key")
2820
+ ? args[args.indexOf("--wallet-private-key") + 1] || ""
2821
+ : "";
2822
+
2823
+ print("\nTraderClaw V1 — Session Auth Test\n");
2824
+ print("=".repeat(50));
2825
+ printInfo(` Orchestrator: ${orchestratorUrl}`);
2826
+ printInfo(` API key: ${pluginConfig.apiKey ? maskKey(pluginConfig.apiKey) : "MISSING"}`);
2827
+ printInfo(` Refresh token: ${effectiveRefreshToken ? maskKey(effectiveRefreshToken) : "MISSING"}`);
2828
+ printInfo(` Sidecar file: ${sidecar ? sessionTokensPath : "not found"}`);
2829
+ printInfo(` Wallet pub key: ${pluginConfig.walletPublicKey || "not set"}`);
2830
+ printInfo(` Wallet priv key: ${getRuntimeWalletPrivateKey(walletPrivateKeyInput) ? "available" : "NOT AVAILABLE"}`);
2831
+ print("");
2832
+
2833
+ const results = [];
2834
+ let currentAccessToken = null;
2835
+ let currentRefreshToken = effectiveRefreshToken;
2836
+
2837
+ // --- Test 1: Initial refresh ---
2838
+ print(" [1/5] Token refresh...");
2839
+ const t1Start = Date.now();
2840
+ try {
2841
+ if (!currentRefreshToken) {
2842
+ throw new Error("No refresh token available — skip to challenge flow test");
2843
+ }
2844
+ const tokens = await doRefresh(orchestratorUrl, currentRefreshToken);
2845
+ if (!tokens) {
2846
+ printWarn(" Refresh returned null (token revoked/expired) — will test challenge flow");
2847
+ results.push({ test: "initial_refresh", status: "expired", ms: Date.now() - t1Start });
2848
+ currentRefreshToken = null;
2849
+ } else {
2850
+ const ms = Date.now() - t1Start;
2851
+ currentAccessToken = tokens.accessToken;
2852
+ currentRefreshToken = tokens.refreshToken;
2853
+ printSuccess(` OK (${ms}ms) — accessTokenTtl: ${tokens.accessTokenTtlSeconds}s, refreshTokenTtl: ${tokens.refreshTokenTtlSeconds}s`);
2854
+ results.push({
2855
+ test: "initial_refresh",
2856
+ status: "ok",
2857
+ ms,
2858
+ accessTokenTtl: tokens.accessTokenTtlSeconds,
2859
+ refreshTokenTtl: tokens.refreshTokenTtlSeconds,
2860
+ tier: tokens.session?.tier,
2861
+ });
2862
+ }
2863
+ } catch (err) {
2864
+ printError(` FAIL: ${err.message}`);
2865
+ results.push({ test: "initial_refresh", status: "fail", ms: Date.now() - t1Start, error: err.message });
2866
+ currentRefreshToken = null;
2867
+ }
2868
+
2869
+ // --- Test 2: Second refresh (verifies rotation worked) ---
2870
+ print(" [2/5] Second refresh (token rotation check)...");
2871
+ const t2Start = Date.now();
2872
+ try {
2873
+ if (!currentRefreshToken) {
2874
+ throw new Error("No refresh token — skipped (previous test failed)");
2875
+ }
2876
+ const tokens2 = await doRefresh(orchestratorUrl, currentRefreshToken);
2877
+ if (!tokens2) {
2878
+ printError(" FAIL: second refresh returned null — rotation may be broken");
2879
+ results.push({ test: "rotation_check", status: "fail", ms: Date.now() - t2Start, error: "null response" });
2880
+ } else {
2881
+ const ms = Date.now() - t2Start;
2882
+ const rotated = tokens2.refreshToken !== currentRefreshToken;
2883
+ currentAccessToken = tokens2.accessToken;
2884
+ currentRefreshToken = tokens2.refreshToken;
2885
+ if (rotated) {
2886
+ printSuccess(` OK (${ms}ms) — refresh token rotated correctly`);
2887
+ } else {
2888
+ printWarn(` OK (${ms}ms) — refresh token NOT rotated (server may use static tokens)`);
2889
+ }
2890
+ results.push({ test: "rotation_check", status: "ok", ms, rotated });
2891
+ }
2892
+ } catch (err) {
2893
+ printError(` FAIL: ${err.message}`);
2894
+ results.push({ test: "rotation_check", status: "fail", ms: Date.now() - t2Start, error: err.message });
2895
+ }
2896
+
2897
+ // --- Test 3: API call with access token ---
2898
+ print(" [3/5] Authenticated API call (/healthz)...");
2899
+ const t3Start = Date.now();
2900
+ try {
2901
+ if (!currentAccessToken) {
2902
+ throw new Error("No access token — skipped");
2903
+ }
2904
+ const health = await httpRequest(`${orchestratorUrl}/healthz`, {
2905
+ accessToken: currentAccessToken,
2906
+ timeout: 8000,
2907
+ });
2908
+ const ms = Date.now() - t3Start;
2909
+ if (health.ok) {
2910
+ printSuccess(` OK (${ms}ms) — orchestrator healthy`);
2911
+ results.push({ test: "api_call", status: "ok", ms });
2912
+ } else {
2913
+ printError(` FAIL: HTTP ${health.status}`);
2914
+ results.push({ test: "api_call", status: "fail", ms, error: `HTTP ${health.status}` });
2915
+ }
2916
+ } catch (err) {
2917
+ printError(` FAIL: ${err.message}`);
2918
+ results.push({ test: "api_call", status: "fail", ms: Date.now() - t3Start, error: err.message });
2919
+ }
2920
+
2921
+ // --- Test 4: Challenge flow (re-auth from scratch) ---
2922
+ print(" [4/5] Challenge flow (full re-authentication)...");
2923
+ const t4Start = Date.now();
2924
+ try {
2925
+ if (!pluginConfig.apiKey) {
2926
+ throw new Error("No API key — cannot test challenge flow");
2927
+ }
2928
+ const challenge = await doChallenge(orchestratorUrl, pluginConfig.apiKey, pluginConfig.walletPublicKey);
2929
+ const ms = Date.now() - t4Start;
2930
+ if (challenge.walletProofRequired) {
2931
+ const wpk = getRuntimeWalletPrivateKey(walletPrivateKeyInput);
2932
+ if (wpk) {
2933
+ try {
2934
+ const walletSig = signChallengeLocally(challenge.challenge, wpk);
2935
+ const tokens = await doSessionStart(
2936
+ orchestratorUrl,
2937
+ pluginConfig.apiKey,
2938
+ challenge.challengeId,
2939
+ challenge.walletPublicKey || pluginConfig.walletPublicKey,
2940
+ walletSig,
2941
+ );
2942
+ const totalMs = Date.now() - t4Start;
2943
+ currentAccessToken = tokens.accessToken;
2944
+ currentRefreshToken = tokens.refreshToken;
2945
+ printSuccess(` OK (${totalMs}ms) — challenge + wallet proof succeeded`);
2946
+ results.push({ test: "challenge_flow", status: "ok", ms: totalMs, walletProof: true });
2947
+ } catch (sigErr) {
2948
+ printError(` FAIL (wallet signing): ${sigErr.message}`);
2949
+ results.push({ test: "challenge_flow", status: "fail", ms: Date.now() - t4Start, error: sigErr.message });
2950
+ }
2951
+ } else {
2952
+ printWarn(` PARTIAL (${ms}ms) — challenge OK but wallet proof needed and TRADERCLAW_WALLET_PRIVATE_KEY not available`);
2953
+ printWarn(" Set TRADERCLAW_WALLET_PRIVATE_KEY env var or pass --wallet-private-key to test fully");
2954
+ results.push({ test: "challenge_flow", status: "partial", ms, walletProofRequired: true, keyAvailable: false });
2955
+ }
2956
+ } else {
2957
+ const tokens = await doSessionStart(orchestratorUrl, pluginConfig.apiKey, challenge.challengeId);
2958
+ const totalMs = Date.now() - t4Start;
2959
+ currentAccessToken = tokens.accessToken;
2960
+ currentRefreshToken = tokens.refreshToken;
2961
+ printSuccess(` OK (${totalMs}ms) — challenge flow succeeded (no wallet proof needed)`);
2962
+ results.push({ test: "challenge_flow", status: "ok", ms: totalMs, walletProof: false });
2963
+ }
2964
+ } catch (err) {
2965
+ printError(` FAIL: ${err.message}`);
2966
+ results.push({ test: "challenge_flow", status: "fail", ms: Date.now() - t4Start, error: err.message });
2967
+ }
2968
+
2969
+ // --- Test 5: Persist rotated tokens back to sidecar ---
2970
+ print(" [5/5] Persist tokens to sidecar...");
2971
+ try {
2972
+ if (currentRefreshToken && currentAccessToken) {
2973
+ mkdirSync(dataDir, { recursive: true });
2974
+ const payload = {
2975
+ refreshToken: currentRefreshToken,
2976
+ accessToken: currentAccessToken,
2977
+ accessTokenExpiresAt: Date.now() + 900_000,
2978
+ walletPublicKey: pluginConfig.walletPublicKey || undefined,
2979
+ };
2980
+ const tmp = `${sessionTokensPath}.${process.pid}.${Date.now()}.tmp`;
2981
+ writeFileSync(tmp, JSON.stringify(payload, null, 2) + "\n", "utf-8");
2982
+ const { renameSync } = await import("fs");
2983
+ renameSync(tmp, sessionTokensPath);
2984
+ printSuccess(` OK — written to ${sessionTokensPath}`);
2985
+ results.push({ test: "persist_sidecar", status: "ok" });
2986
+
2987
+ pluginConfig.refreshToken = currentRefreshToken;
2988
+ removeLegacyWalletPrivateKey(pluginConfig);
2989
+ setPluginConfig(config, pluginConfig);
2990
+ writeConfig(config);
2991
+ printSuccess(" Config updated with latest refresh token");
2992
+ } else {
2993
+ printWarn(" SKIP — no valid tokens to persist");
2994
+ results.push({ test: "persist_sidecar", status: "skip" });
2995
+ }
2996
+ } catch (err) {
2997
+ printError(` FAIL: ${err.message}`);
2998
+ results.push({ test: "persist_sidecar", status: "fail", error: err.message });
2999
+ }
3000
+
3001
+ // --- Summary ---
3002
+ print("\n" + "=".repeat(50));
3003
+ const passed = results.filter((r) => r.status === "ok").length;
3004
+ const failed = results.filter((r) => r.status === "fail").length;
3005
+ const partial = results.filter((r) => r.status === "partial" || r.status === "expired" || r.status === "skip").length;
3006
+
3007
+ if (failed === 0 && passed > 0) {
3008
+ printSuccess(`\n ALL TESTS PASSED (${passed}/${results.length})`);
3009
+ } else if (failed > 0) {
3010
+ printError(`\n ${failed} FAILED, ${passed} passed, ${partial} skipped/partial`);
3011
+ } else {
3012
+ printWarn(`\n ${passed} passed, ${partial} skipped/partial`);
3013
+ }
3014
+
3015
+ print("\n Build-and-test workflow (no reinstall):");
3016
+ printInfo(" cd /path/to/plugin && npm run build");
3017
+ printInfo(" npm link");
3018
+ printInfo(" sudo systemctl restart openclaw");
3019
+ printInfo(" traderclaw test-session");
3020
+ print("");
3021
+
3022
+ if (failed > 0) process.exit(1);
3023
+ }
3024
+
3025
+ function printHelp() {
3026
+ print(`
3027
+ TraderClaw V1 CLI v${VERSION}
3028
+
3029
+ Usage: traderclaw <command> [options]
3030
+
3031
+ Commands:
3032
+ setup Set up the plugin (signup or API key, session, wallet)
3033
+ signup Create a new account (alias for: setup --signup; run locally, not via the agent)
3034
+ precheck Run environment checks (dry-run or allow-install)
3035
+ install Launch installer flows (--wizard for localhost GUI)
3036
+ gateway Gateway helpers (see subcommands below)
3037
+ login Re-authenticate (uses refresh token when valid; full challenge only if needed)
3038
+ logout Revoke current session and clear tokens
3039
+ status Check connection health and wallet status
3040
+ config View and manage configuration
3041
+ test-session Test session auth flow (refresh, rotation, challenge) without reinstalling
3042
+
3043
+ Setup options:
3044
+ --api-key, -k API key (skip interactive prompt)
3045
+ --url, -u Orchestrator URL (skip interactive prompt)
3046
+ --user-id External user ID for signup
3047
+ --wallet-private-key Optional base58 private key for wallet proof flow (runtime only, never saved)
3048
+ --gateway-base-url, -g Gateway public HTTPS URL for orchestrator callbacks
3049
+ --gateway-token, -t Gateway bearer token (defaults to API key)
3050
+ --skip-gateway-registration Skip gateway URL registration with orchestrator
3051
+ --show-api-key Extra hint after signup (full key is always shown once; confirm with API_KEY_STORED)
3052
+ --show-wallet-private-key Reveal full wallet private key in setup output
3053
+ --signup Force signup flow (create new account)
3054
+ --write-gateway-env Write TRADERCLAW_WALLET_PRIVATE_KEY to a systemd EnvironmentFile for the user gateway (Linux)
3055
+ --no-ensure-gateway-persistent Skip automatic Linux loginctl linger + user unit enable after setup
3056
+
3057
+ Gateway subcommands:
3058
+ gateway ensure-persistent Linux: enable loginctl linger and systemd --user unit for OpenClaw gateway
3059
+
3060
+ Login options:
3061
+ --wallet-private-key <k> Base58 key for wallet proof when the server requires it (runtime only)
3062
+ --force-reauth Clear refresh token and run full API challenge (use after logout or to rotate session)
3063
+
3064
+ Config subcommands:
3065
+ config show Show current configuration
3066
+ config set <k> <v> Update a configuration value
3067
+ config reset Remove plugin configuration
3068
+
3069
+ Examples:
3070
+ traderclaw signup
3071
+ traderclaw setup
3072
+ traderclaw login --wallet-private-key <base58_key>
3073
+ TRADERCLAW_WALLET_PRIVATE_KEY=<base58_key> traderclaw login
3074
+ traderclaw precheck --dry-run --output precheck.log
3075
+ traderclaw precheck --allow-install
3076
+ traderclaw install --wizard
3077
+ traderclaw install --wizard --lane quick-local
3078
+ traderclaw gateway ensure-persistent
3079
+ traderclaw setup --signup --user-id my_agent_001
3080
+ traderclaw setup --api-key oc_xxx --url https://api.traderclaw.ai
3081
+ traderclaw setup --gateway-base-url https://gateway.myhost.ts.net
3082
+ traderclaw login
3083
+ traderclaw login --force-reauth --wallet-private-key <base58_key>
3084
+ traderclaw logout
3085
+ traderclaw status
3086
+ traderclaw config show
3087
+ traderclaw config set apiTimeout 60000
3088
+ traderclaw test-session
3089
+ traderclaw test-session --wallet-private-key <base58_key>
3090
+ `);
3091
+ }
3092
+
3093
+ async function main() {
3094
+ const args = process.argv.slice(2);
3095
+ const command = args[0];
3096
+
3097
+ if (!command || command === "--help" || command === "-h") {
3098
+ printHelp();
3099
+ process.exit(0);
3100
+ }
3101
+
3102
+ if (command === "--version" || command === "-v") {
3103
+ print(`traderclaw v${VERSION}`);
3104
+ process.exit(0);
3105
+ }
3106
+
3107
+ switch (command) {
3108
+ case "setup":
3109
+ await cmdSetup(args.slice(1));
3110
+ break;
3111
+ case "signup":
3112
+ await cmdSetup(["--signup", ...args.slice(1)]);
3113
+ break;
3114
+ case "precheck":
3115
+ await cmdPrecheck(args.slice(1));
3116
+ break;
3117
+ case "install":
3118
+ await cmdInstall(args.slice(1));
3119
+ break;
3120
+ case "gateway":
3121
+ await cmdGateway(args.slice(1));
3122
+ break;
3123
+ case "login":
3124
+ await cmdLogin(args.slice(1));
3125
+ break;
3126
+ case "logout":
3127
+ await cmdLogout();
3128
+ break;
3129
+ case "status":
3130
+ await cmdStatus();
3131
+ break;
3132
+ case "config":
3133
+ await cmdConfig(args.slice(1));
3134
+ break;
3135
+ case "test-session":
3136
+ await cmdTestSession(args.slice(1));
3137
+ break;
3138
+ default:
3139
+ printError(`Unknown command: ${command}`);
3140
+ printHelp();
3141
+ process.exit(1);
3142
+ }
3143
+ }
3144
+
3145
+ main().catch((err) => {
3146
+ printError(err.message || String(err));
3147
+ process.exit(1);
3148
+ });