traderclaw-cli 1.0.116 → 1.0.120

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.
@@ -268,14 +268,63 @@ function commandExists(cmd) {
268
268
  }
269
269
  }
270
270
 
271
- function getCommandOutput(cmd) {
271
+ function getCommandOutput(cmd, { timeoutMs = 0 } = {}) {
272
272
  try {
273
- return execSync(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], shell: true, maxBuffer: 50 * 1024 * 1024, env: NO_COLOR_ENV }).trim();
273
+ const opts = {
274
+ encoding: "utf-8",
275
+ stdio: ["pipe", "pipe", "pipe"],
276
+ shell: true,
277
+ maxBuffer: 50 * 1024 * 1024,
278
+ env: NO_COLOR_ENV,
279
+ };
280
+ if (timeoutMs > 0) opts.timeout = timeoutMs;
281
+ return execSync(cmd, opts).trim();
274
282
  } catch {
275
283
  return null;
276
284
  }
277
285
  }
278
286
 
287
+ /**
288
+ * First existing `skills/solana-trader` directory: local package → OpenClaw extension → global npm.
289
+ * @param {{ pluginId: string, pluginPackage: string }} modeConfig
290
+ * @returns {string|null}
291
+ */
292
+ export function resolveSolanaTraderPackagedRoot(modeConfig) {
293
+ const candidates = [
294
+ join(PLUGIN_PACKAGE_ROOT, "skills", "solana-trader"),
295
+ join(homedir(), ".openclaw", "extensions", modeConfig.pluginId, "skills", "solana-trader"),
296
+ ];
297
+ const npmRoot = getCommandOutput("npm root -g");
298
+ if (npmRoot) {
299
+ candidates.push(join(npmRoot, modeConfig.pluginPackage, "skills", "solana-trader"));
300
+ }
301
+ for (const dir of candidates) {
302
+ if (existsSync(dir)) return dir;
303
+ }
304
+ return null;
305
+ }
306
+
307
+ /**
308
+ * First existing gateway template file under `config/{filename}`.
309
+ * @param {{ pluginId: string, pluginPackage: string }} modeConfig
310
+ * @param {string} gatewayConfigFilename
311
+ * @returns {string|null}
312
+ */
313
+ function resolveGatewayConfigSourcePath(modeConfig, gatewayConfigFilename) {
314
+ const candidates = [
315
+ join(PLUGIN_PACKAGE_ROOT, "config", gatewayConfigFilename),
316
+ join(homedir(), ".openclaw", "extensions", modeConfig.pluginId, "config", gatewayConfigFilename),
317
+ ];
318
+ const npmRoot = getCommandOutput("npm root -g");
319
+ if (npmRoot) {
320
+ candidates.push(join(npmRoot, modeConfig.pluginPackage, "config", gatewayConfigFilename));
321
+ }
322
+ for (const p of candidates) {
323
+ if (existsSync(p)) return p;
324
+ }
325
+ return null;
326
+ }
327
+
279
328
  function extractUrls(text = "") {
280
329
  const matches = text.match(/https?:\/\/[^\s"')]+/g);
281
330
  return matches ? [...new Set(matches)] : [];
@@ -511,12 +560,35 @@ export async function ensureOpenClawGlobalPackageDependencies() {
511
560
  * global from an older install causes `openclaw plugins install` to fail config validation with
512
561
  * "plugin requires OpenClaw >=… but this host is …".
513
562
  */
514
- async function installOpenClawPlatform() {
563
+ /** Bound `openclaw --version` so a post-OAuth CLI that blocks on gateway/device init cannot hang the wizard. */
564
+ const OPENCLAW_CLI_VERSION_TIMEOUT_MS = 25_000;
565
+
566
+ async function installOpenClawPlatform(onEvent) {
515
567
  const hadOpenclaw = commandExists("openclaw");
516
- const previousVersion = hadOpenclaw ? getCommandOutput("openclaw --version") : null;
517
- await runCommandWithEvents("npm", ["install", "-g", "--ignore-scripts", "--registry", "https://registry.npmjs.org/", `openclaw@${OPENCLAW_VERSION}`]);
568
+ const previousVersion = hadOpenclaw ? getCommandOutput("openclaw --version", { timeoutMs: OPENCLAW_CLI_VERSION_TIMEOUT_MS }) : null;
569
+ if (hadOpenclaw && !previousVersion && typeof onEvent === "function") {
570
+ onEvent({
571
+ type: "stderr",
572
+ text: "openclaw --version did not return in time (or failed); continuing with npm install -g anyway.\n",
573
+ urls: [],
574
+ });
575
+ }
576
+ const npmCwd = getNpmGlobalInstallCwd();
577
+ await runCommandWithEvents("npm", ["install", "-g", "--ignore-scripts", "--registry", "https://registry.npmjs.org/", `openclaw@${OPENCLAW_VERSION}`], {
578
+ onEvent,
579
+ cwd: npmCwd,
580
+ shell: false,
581
+ });
518
582
  const available = commandExists("openclaw");
519
- const version = available ? getCommandOutput("openclaw --version") : null;
583
+ let version = available ? getCommandOutput("openclaw --version", { timeoutMs: OPENCLAW_CLI_VERSION_TIMEOUT_MS }) : null;
584
+ if (available && !version && typeof onEvent === "function") {
585
+ onEvent({
586
+ type: "stderr",
587
+ text: "openclaw is on PATH but --version timed out; treating install as successful.\n",
588
+ urls: [],
589
+ });
590
+ version = "(version check timed out)";
591
+ }
520
592
  if (!available) {
521
593
  throw new Error(`npm install -g openclaw@${OPENCLAW_VERSION} finished but \`openclaw\` is not available on PATH`);
522
594
  }
@@ -726,6 +798,31 @@ async function installAndEnableOpenClawPlugin(modeConfig, onEvent, orchestratorU
726
798
  };
727
799
  }
728
800
 
801
+ /**
802
+ * Idempotent: ensure OpenClaw discovers skills under ~/.openclaw/extensions/<pluginId>/skills (extraDirs).
803
+ * See OpenClaw workspace skill loader: config.skills.load.extraDirs → openclaw-extra.
804
+ * @param {Record<string, unknown>} config
805
+ * @param {string} pluginId
806
+ */
807
+ function ensureTraderSkillsExtraDir(config, pluginId) {
808
+ const marker = `.openclaw/extensions/${pluginId}/skills`;
809
+ const tildeEntry = `~/.openclaw/extensions/${pluginId}/skills`;
810
+ if (!config.skills || typeof config.skills !== "object") config.skills = {};
811
+ if (!config.skills.load || typeof config.skills.load !== "object") config.skills.load = {};
812
+ const raw = config.skills.load.extraDirs;
813
+ const dirs = Array.isArray(raw) ? [...raw] : [];
814
+ const normalized = (d) => (typeof d === "string" ? d.replace(/\\/g, "/") : "");
815
+ const needle = normalized(tildeEntry);
816
+ const hasMarker = dirs.some((d) => {
817
+ const n = normalized(d);
818
+ return n.includes(marker) || n === needle;
819
+ });
820
+ if (!hasMarker) {
821
+ dirs.push(tildeEntry);
822
+ config.skills.load.extraDirs = dirs;
823
+ }
824
+ }
825
+
729
826
  function seedPluginConfig(modeConfig, orchestratorUrl, configPath = CONFIG_FILE) {
730
827
  const defaultUrl = orchestratorUrl || "https://api.traderclaw.ai";
731
828
 
@@ -763,6 +860,8 @@ function seedPluginConfig(modeConfig, orchestratorUrl, configPath = CONFIG_FILE)
763
860
 
764
861
  mergeOrchestratorForId(modeConfig.pluginId);
765
862
 
863
+ ensureTraderSkillsExtraDir(config, modeConfig.pluginId);
864
+
766
865
  // Do not set plugins.allow here: OpenClaw validates allow[] against the plugin registry, and
767
866
  // the id is not registered until after `openclaw plugins install`. Pre-seeding allow caused:
768
867
  // "plugins.allow: plugin not found: <id>".
@@ -1285,10 +1384,8 @@ function deployGatewayConfig(modeConfig) {
1285
1384
  const gatewayDir = join(CONFIG_DIR, "gateway");
1286
1385
  mkdirSync(gatewayDir, { recursive: true });
1287
1386
  const destFile = join(gatewayDir, modeConfig.gatewayConfig);
1288
- const npmRoot = getCommandOutput("npm root -g");
1289
- if (!npmRoot) return { deployed: false, dest: destFile };
1290
- const src = join(npmRoot, modeConfig.pluginPackage, "config", modeConfig.gatewayConfig);
1291
- if (!existsSync(src)) return { deployed: false, dest: destFile };
1387
+ const src = resolveGatewayConfigSourcePath(modeConfig, modeConfig.gatewayConfig);
1388
+ if (!src) return { deployed: false, dest: destFile };
1292
1389
  writeFileSync(destFile, readFileSync(src));
1293
1390
  return { deployed: true, source: src, dest: destFile };
1294
1391
  }
@@ -1325,13 +1422,13 @@ export function resolveAgentWorkspaceDir(configPath = CONFIG_FILE) {
1325
1422
  }
1326
1423
 
1327
1424
  /**
1328
- * Copy skills/solana-trader/HEARTBEAT.md from the globally installed npm package into the workspace root.
1425
+ * Copy skills/solana-trader/HEARTBEAT.md from the plugin package, OpenClaw extension, or global npm into the workspace root.
1329
1426
  * Skips overwrite if a non-empty file already exists (user may have customized it).
1330
1427
  */
1331
1428
  export function deployWorkspaceHeartbeat(modeConfig) {
1332
- const npmRoot = getCommandOutput("npm root -g");
1333
- if (!npmRoot) return { deployed: false, reason: "npm_root_g_failed" };
1334
- const src = join(npmRoot, modeConfig.pluginPackage, "skills", "solana-trader", "HEARTBEAT.md");
1429
+ const skillRoot = resolveSolanaTraderPackagedRoot(modeConfig);
1430
+ if (!skillRoot) return { deployed: false, reason: "source_missing" };
1431
+ const src = join(skillRoot, "HEARTBEAT.md");
1335
1432
  if (!existsSync(src)) return { deployed: false, reason: "source_missing", src };
1336
1433
 
1337
1434
  const workspaceDir = resolveAgentWorkspaceDir(CONFIG_FILE);
@@ -1358,10 +1455,10 @@ export function deployWorkspaceHeartbeat(modeConfig) {
1358
1455
  * Skips files that already exist and are non-empty so user customisations are preserved.
1359
1456
  */
1360
1457
  export function deployWorkspaceBootstrapFiles(modeConfig) {
1361
- const npmRoot = getCommandOutput("npm root -g");
1362
- if (!npmRoot) return { deployed: [], skipped: [], failed: [], reason: "npm_root_g_failed" };
1458
+ const skillRoot = resolveSolanaTraderPackagedRoot(modeConfig);
1459
+ if (!skillRoot) return { deployed: [], skipped: [], failed: [], reason: "source_dir_missing" };
1363
1460
 
1364
- const srcDir = join(npmRoot, modeConfig.pluginPackage, "skills", "solana-trader", "workspace");
1461
+ const srcDir = join(skillRoot, "workspace");
1365
1462
  if (!existsSync(srcDir)) return { deployed: [], skipped: [], failed: [], reason: "source_dir_missing", srcDir };
1366
1463
 
1367
1464
  const workspaceDir = resolveAgentWorkspaceDir(CONFIG_FILE);
@@ -2039,6 +2136,8 @@ export class InstallerStepEngine {
2039
2136
  skipInstallOpenClaw: options.skipInstallOpenClaw === true,
2040
2137
  skipInstallPlugin: options.skipInstallPlugin === true,
2041
2138
  skipTailscale: options.skipTailscale === true,
2139
+ /** When true (e.g. EC2 bootstrap before Tailscale auth), gateway starts without `tailscale funnel`. */
2140
+ skipFunnel: options.skipFunnel === true,
2042
2141
  skipGatewayBootstrap: options.skipGatewayBootstrap === true,
2043
2142
  skipGatewayConfig: options.skipGatewayConfig === true,
2044
2143
  // Wizard / CLI — must be preserved for seedXConfig
@@ -2489,7 +2588,11 @@ export class InstallerStepEngine {
2489
2588
  }
2490
2589
 
2491
2590
  if (!this.options.skipInstallOpenClaw) {
2492
- await this.runStep("install_openclaw", "Installing or upgrading OpenClaw platform", async () => installOpenClawPlatform());
2591
+ await this.runStep("install_openclaw", "Installing or upgrading OpenClaw platform", async () =>
2592
+ installOpenClawPlatform((evt) =>
2593
+ this.emitLog("install_openclaw", evt.type === "stderr" ? "warn" : "info", evt.text, evt.urls || []),
2594
+ ),
2595
+ );
2493
2596
  }
2494
2597
 
2495
2598
  // Non-fatal: warn when the CLI has devices in pending-approval or repair state.
@@ -2600,6 +2703,14 @@ export class InstallerStepEngine {
2600
2703
  );
2601
2704
  await this.runWithPrivilegeGuidance("gateway_bootstrap", "openclaw", ["gateway", "install"]);
2602
2705
  await this.runWithPrivilegeGuidance("gateway_bootstrap", "openclaw", ["gateway", "restart"]);
2706
+ if (this.options.skipFunnel) {
2707
+ this.emitLog(
2708
+ "gateway_bootstrap",
2709
+ "info",
2710
+ "Skipping Tailscale funnel (skipFunnel). After `tailscale up`, run: traderclaw install --headless --funnel-only",
2711
+ );
2712
+ return { funnelSkipped: true };
2713
+ }
2603
2714
  return this.runFunnel();
2604
2715
  } catch (err) {
2605
2716
  const text = `${err?.message || ""}\n${err?.stderr || ""}\n${err?.stdout || ""}`.toLowerCase();
@@ -2615,6 +2726,9 @@ export class InstallerStepEngine {
2615
2726
  if (gatewayStartFailed || gatewayModeUnset) {
2616
2727
  const recovered = await this.tryAutoRecoverGatewayMode("gateway_bootstrap");
2617
2728
  if (recovered.success) {
2729
+ if (this.options.skipFunnel) {
2730
+ return { funnelSkipped: true, recovered: true };
2731
+ }
2618
2732
  return this.runFunnel();
2619
2733
  }
2620
2734
  if (gatewayModeUnset) {
@@ -2806,6 +2920,28 @@ export class InstallerStepEngine {
2806
2920
  }
2807
2921
  }
2808
2922
 
2923
+ /**
2924
+ * After Tailscale login (`tailscale up`), expose loopback gateway :18789 via funnel.
2925
+ */
2926
+ export async function tailscaleFunnelOpenclaw18789(options = {}) {
2927
+ const onEvt = typeof options.onEvent === "function" ? options.onEvent : () => {};
2928
+ try {
2929
+ await runCommandWithEvents(
2930
+ "tailscale",
2931
+ ["funnel", "--bg", "18789"],
2932
+ { onEvent: (evt) => onEvt(evt) },
2933
+ );
2934
+ } catch (err) {
2935
+ const details = `${err?.stderr || ""}\n${err?.stdout || ""}\n${err?.message || ""}`.toLowerCase();
2936
+ if (details.includes("access denied") || details.includes("operator")) {
2937
+ throw new Error(tailscalePermissionRemediation());
2938
+ }
2939
+ throw err;
2940
+ }
2941
+ const statusOut = getCommandOutput("tailscale funnel status") || "";
2942
+ return { funnelUrl: firstUrl(statusOut) || null, rawStatus: statusOut };
2943
+ }
2944
+
2809
2945
  export function assertWizardXCredentials(modeConfig, options = {}) {
2810
2946
  const t = (s) => (typeof s === "string" ? s.trim() : "");
2811
2947
  const o = options || {};
@@ -2135,6 +2135,10 @@ function parseInstallWizardArgs(args) {
2135
2135
  if ((key === "--gateway-token" || key === "-t") && next) out.gatewayToken = args[++i];
2136
2136
  if (key === "--with-telegram") out.enableTelegram = true;
2137
2137
  if (key === "--telegram-token" && next) out.telegramToken = args[++i];
2138
+ if (key === "--x-consumer-key" && next) out.xConsumerKey = args[++i];
2139
+ if (key === "--x-consumer-secret" && next) out.xConsumerSecret = args[++i];
2140
+ if (key === "--x-access-token-main" && next) out.xAccessTokenMain = args[++i];
2141
+ if (key === "--x-access-token-main-secret" && next) out.xAccessTokenMainSecret = args[++i];
2138
2142
  }
2139
2143
  return out;
2140
2144
  }
@@ -3598,10 +3602,163 @@ function wizardHtml(defaults) {
3598
3602
  </html>`;
3599
3603
  }
3600
3604
 
3605
+ function traderclawHeadlessEnv(name) {
3606
+ return String(process.env[name] || "").trim();
3607
+ }
3608
+
3609
+ function parseInstallHeadlessMeta(args) {
3610
+ const funnelOnly = args.includes("--funnel-only");
3611
+ const skipTailscale =
3612
+ args.includes("--skip-tailscale") || traderclawHeadlessEnv("TRADERCLAW_INSTALL_SKIP_TAILSCALE") === "1";
3613
+ let skipFunnel = args.includes("--skip-funnel") || traderclawHeadlessEnv("TRADERCLAW_INSTALL_SKIP_FUNNEL") === "1";
3614
+ if (skipTailscale) skipFunnel = true;
3615
+ return { funnelOnly, skipTailscale, skipFunnel };
3616
+ }
3617
+
3618
+ function filterHeadlessArgv(args) {
3619
+ const strip = new Set(["--headless", "--funnel-only", "--skip-tailscale", "--skip-funnel"]);
3620
+ return args.filter((a) => !strip.has(a));
3621
+ }
3622
+
3623
+ async function cmdInstallHeadless(args) {
3624
+ const meta = parseInstallHeadlessMeta(args);
3625
+ const { tailscaleFunnelOpenclaw18789, createInstallerStepEngine, assertWizardXCredentials } = await import(
3626
+ "./installer-step-engine.mjs",
3627
+ );
3628
+
3629
+ if (meta.funnelOnly) {
3630
+ print("\nTraderClaw install — Tailscale funnel (public HTTPS → gateway loopback :18789)\n");
3631
+ try {
3632
+ const r = await tailscaleFunnelOpenclaw18789({
3633
+ onEvent: (evt) => {
3634
+ const t = typeof evt?.text === "string" ? evt.text : "";
3635
+ if (t.trim()) printInfo(t.trimEnd());
3636
+ },
3637
+ });
3638
+ if (r.funnelUrl) printSuccess(`Public gateway URL (Tailscale funnel): ${r.funnelUrl}`);
3639
+ else printWarn("Could not parse funnel URL — on the VPS run: tailscale funnel status");
3640
+ print("");
3641
+ } catch (err) {
3642
+ printError(err?.message || String(err));
3643
+ process.exit(1);
3644
+ }
3645
+ printInfo(
3646
+ "Use this URL as gatewayBaseUrl when running: traderclaw setup --gateway-base-url <url> --gateway-token <token>",
3647
+ );
3648
+ return;
3649
+ }
3650
+
3651
+ let w = parseInstallWizardArgs(filterHeadlessArgv(args));
3652
+ w.llmProvider = w.llmProvider || traderclawHeadlessEnv("TRADERCLAW_HEADLESS_LLM_PROVIDER");
3653
+ w.llmModel = w.llmModel || traderclawHeadlessEnv("TRADERCLAW_HEADLESS_LLM_MODEL");
3654
+ w.llmCredential =
3655
+ w.llmCredential
3656
+ || traderclawHeadlessEnv("TRADERCLAW_HEADLESS_LLM_API_KEY")
3657
+ || traderclawHeadlessEnv("TRADERCLAW_LLM_API_KEY");
3658
+ w.telegramToken =
3659
+ w.telegramToken
3660
+ || traderclawHeadlessEnv("TRADERCLAW_HEADLESS_TELEGRAM_TOKEN")
3661
+ || traderclawHeadlessEnv("OPENCLAW_TELEGRAM_BOT_TOKEN");
3662
+ w.apiKey =
3663
+ w.apiKey
3664
+ || traderclawHeadlessEnv("TRADERCLAW_HEADLESS_ORCHESTRATOR_API_KEY")
3665
+ || traderclawHeadlessEnv("TRADERCLAW_API_KEY");
3666
+ if (!(w.orchestratorUrl || "").trim()) {
3667
+ w.orchestratorUrl = traderclawHeadlessEnv("TRADERCLAW_ORCHESTRATOR_URL") || "https://api.traderclaw.ai";
3668
+ }
3669
+ w.xConsumerKey = w.xConsumerKey || traderclawHeadlessEnv("X_CONSUMER_KEY");
3670
+ w.xConsumerSecret = w.xConsumerSecret || traderclawHeadlessEnv("X_CONSUMER_SECRET");
3671
+ w.xAccessTokenMain = w.xAccessTokenMain || traderclawHeadlessEnv("X_ACCESS_TOKEN_MAIN");
3672
+ w.xAccessTokenMainSecret =
3673
+ w.xAccessTokenMainSecret || traderclawHeadlessEnv("X_ACCESS_TOKEN_MAIN_SECRET");
3674
+
3675
+ if (w.llmAuthMode === "oauth") {
3676
+ printError("traderclaw install --headless does not support LLM OAuth — use traderclaw install --wizard");
3677
+ process.exit(1);
3678
+ }
3679
+ if (!w.llmProvider || !w.llmModel || !w.llmCredential) {
3680
+ printError(
3681
+ "Headless install requires --llm-provider, --llm-model, --llm-api-key (env: TRADERCLAW_HEADLESS_LLM_* ).",
3682
+ );
3683
+ process.exit(1);
3684
+ }
3685
+ if (!w.telegramToken) {
3686
+ printError("Headless install requires --telegram-token (env: TRADERCLAW_HEADLESS_TELEGRAM_TOKEN).");
3687
+ process.exit(1);
3688
+ }
3689
+
3690
+ const modeConfig = {
3691
+ pluginPackage: "solana-traderclaw",
3692
+ pluginId: "solana-trader",
3693
+ cliName: "traderclaw",
3694
+ gatewayConfig: "gateway-v1.json5",
3695
+ agents: ["cto", "onchain-analyst", "alpha-signal-analyst", "risk-officer", "strategy-researcher"],
3696
+ };
3697
+
3698
+ const xErr = assertWizardXCredentials(modeConfig, w);
3699
+ if (xErr) {
3700
+ printError(xErr);
3701
+ process.exit(1);
3702
+ }
3703
+
3704
+ print("\nTraderClaw install — headless (non-interactive)\n");
3705
+ const engine = createInstallerStepEngine(
3706
+ modeConfig,
3707
+ {
3708
+ lane: w.lane === "quick-local" ? "quick-local" : "event-driven",
3709
+ llmAuthMode: "api_key",
3710
+ llmProvider: w.llmProvider,
3711
+ llmModel: w.llmModel,
3712
+ llmCredential: w.llmCredential,
3713
+ apiKey: w.apiKey || "",
3714
+ orchestratorUrl: String(w.orchestratorUrl || "https://api.traderclaw.ai").replace(/\/+$/, ""),
3715
+ gatewayBaseUrl: w.gatewayBaseUrl || "",
3716
+ gatewayToken: w.gatewayToken || "",
3717
+ enableTelegram: true,
3718
+ telegramToken: w.telegramToken,
3719
+ skipTailscale: meta.skipTailscale,
3720
+ skipFunnel: meta.skipFunnel,
3721
+ xConsumerKey: w.xConsumerKey,
3722
+ xConsumerSecret: w.xConsumerSecret,
3723
+ xAccessTokenMain: w.xAccessTokenMain,
3724
+ xAccessTokenMainSecret: w.xAccessTokenMainSecret,
3725
+ },
3726
+ {
3727
+ onLog: ({ text }) => {
3728
+ if (text) print(String(text));
3729
+ },
3730
+ onStepEvent: ({ stepId, status, detail }) => {
3731
+ print(`[install] ${stepId} ${status}${detail ? ` — ${detail}` : ""}`);
3732
+ },
3733
+ },
3734
+ );
3735
+
3736
+ const state = await engine.runAll();
3737
+ if (state.status !== "completed") {
3738
+ printError("Headless install failed.");
3739
+ process.exit(1);
3740
+ }
3741
+
3742
+ printSuccess("\nHeadless install completed.");
3743
+ if (state.setupHandoff?.command) {
3744
+ printInfo(`CLI hand-off: ${state.setupHandoff.command}`);
3745
+ }
3746
+ if (meta.skipFunnel) {
3747
+ printWarn(
3748
+ "Funnel skipped — run `tailscale up` then `traderclaw install --headless --funnel-only`, then traderclaw setup with --gateway-base-url.",
3749
+ );
3750
+ }
3751
+ }
3752
+
3601
3753
  async function cmdInstall(args) {
3754
+ const headless = args.includes("--headless");
3755
+ if (headless) {
3756
+ await cmdInstallHeadless(args);
3757
+ return;
3758
+ }
3602
3759
  const wizard = args.includes("--wizard");
3603
3760
  if (!wizard) {
3604
- printError("Only wizard mode is currently supported. Use: traderclaw install --wizard");
3761
+ printError('Use: traderclaw install --wizard or traderclaw install --headless (--help)');
3605
3762
  process.exit(1);
3606
3763
  }
3607
3764
 
@@ -4196,6 +4353,10 @@ async function cmdInstall(args) {
4196
4353
  return;
4197
4354
  }
4198
4355
 
4356
+ for (const id of [...oauthSessions.keys()]) {
4357
+ killOauthSession(id);
4358
+ }
4359
+
4199
4360
  running = true;
4200
4361
  runtime.status = "running";
4201
4362
  runtime.logs = [];
@@ -4628,6 +4789,16 @@ Install wizard (traderclaw install --wizard):
4628
4789
  --llm-oauth-paste Paste redirect URL or code for Codex OAuth (non-skip)
4629
4790
  --llm-oauth-skip-login Skip login when you already ran openclaw models auth login
4630
4791
 
4792
+ Headless VPS / CI (traderclaw install --headless):
4793
+ Requires API-key LLM (no OAuth): --llm-provider, --llm-model, --llm-api-key, --telegram-token
4794
+ Same flags as wizard for --api-key/-k, --url/-u, --gateway-base-url/-g, X/Twitter (optional, all-or-none):
4795
+ --x-consumer-key ... --x-consumer-secret ... --x-access-token-main ... --x-access-token-main-secret ...
4796
+ --skip-tailscale Skip Tailscale install/up (implies --skip-funnel unless overridden)
4797
+ --skip-funnel Do not run tailscale funnel after gateway restart
4798
+ --funnel-only After manual tailscale up: publish gateway :18789 via funnel only
4799
+ Env: TRADERCLAW_HEADLESS_LLM_PROVIDER/MODEL/API_KEY, TRADERCLAW_HEADLESS_TELEGRAM_TOKEN,
4800
+ TRADERCLAW_HEADLESS_ORCHESTRATOR_API_KEY, TRADERCLAW_INSTALL_SKIP_TAILSCALE/FUNNEL, X_* etc.
4801
+
4631
4802
  Examples:
4632
4803
  traderclaw signup
4633
4804
  traderclaw setup
@@ -4638,6 +4809,9 @@ Examples:
4638
4809
  traderclaw install --wizard
4639
4810
  traderclaw install --wizard --lane quick-local
4640
4811
  traderclaw install --wizard --llm-auth oauth --llm-provider openai-codex --llm-oauth-skip-login
4812
+ traderclaw install --headless --skip-tailscale --llm-provider anthropic --llm-model anthropic/claude-sonnet-4-6 \\
4813
+ --llm-api-key \"$ANTHROPIC_KEY\" --telegram-token \"$TG\" --skip-funnel
4814
+ traderclaw install --headless --funnel-only
4641
4815
  traderclaw repair-openclaw
4642
4816
  traderclaw gateway ensure-persistent
4643
4817
  traderclaw setup --signup --user-id my_agent_001 --referral-code ABCD1234
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "traderclaw-cli",
3
- "version": "1.0.116",
3
+ "version": "1.0.120",
4
4
  "description": "Global TraderClaw CLI (install --wizard, setup, precheck). Installs solana-traderclaw as a dependency for OpenClaw plugin files.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,7 +17,7 @@
17
17
  "node": ">=22"
18
18
  },
19
19
  "dependencies": {
20
- "solana-traderclaw": "^1.0.116"
20
+ "solana-traderclaw": "^1.0.120"
21
21
  },
22
22
  "keywords": [
23
23
  "traderclaw",