traderclaw-cli 1.0.117 → 1.0.121

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.
@@ -284,6 +284,47 @@ function getCommandOutput(cmd, { timeoutMs = 0 } = {}) {
284
284
  }
285
285
  }
286
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
+
287
328
  function extractUrls(text = "") {
288
329
  const matches = text.match(/https?:\/\/[^\s"')]+/g);
289
330
  return matches ? [...new Set(matches)] : [];
@@ -410,7 +451,25 @@ function isOpenClawConfigSchemaFailure(text) {
410
451
 
411
452
  function runCommandWithEvents(cmd, args = [], opts = {}) {
412
453
  return new Promise((resolve, reject) => {
413
- const { onEvent, ...spawnOpts } = opts;
454
+ const {
455
+ onEvent,
456
+ timeoutMs = 0,
457
+ heartbeatMs = 0,
458
+ heartbeatText = "command still running…",
459
+ ...spawnOpts
460
+ } = opts;
461
+
462
+ let settled = false;
463
+ let timeoutId;
464
+ let heartbeatId;
465
+ const finish = (fn, arg) => {
466
+ if (settled) return;
467
+ settled = true;
468
+ if (timeoutId) clearTimeout(timeoutId);
469
+ if (heartbeatId) clearInterval(heartbeatId);
470
+ fn(arg);
471
+ };
472
+
414
473
  const isNpm = /(?:^|[\\/])npm(?:\.cmd)?$/.test(cmd) || cmd === "npm";
415
474
  if (isNpm && !spawnOpts.env?.NODE_OPTIONS?.includes("max-old-space-size")) {
416
475
  spawnOpts.env = {
@@ -430,6 +489,44 @@ function runCommandWithEvents(cmd, args = [], opts = {}) {
430
489
  const emitFn = typeof onEvent === "function" ? onEvent : null;
431
490
  const emit = (event) => emitFn && emitFn(event);
432
491
 
492
+ if (typeof timeoutMs === "number" && timeoutMs > 0) {
493
+ timeoutId = setTimeout(() => {
494
+ try {
495
+ child.kill("SIGTERM");
496
+ } catch {
497
+ /* ignore */
498
+ }
499
+ setTimeout(() => {
500
+ try {
501
+ child.kill("SIGKILL");
502
+ } catch {
503
+ /* ignore */
504
+ }
505
+ }, 12_000);
506
+ const tail = `${stdout}\n${stderr}`.trim().slice(-6000);
507
+ const err = new Error(
508
+ `Timed out after ${timeoutMs}ms: ${cmd} ${args.join(" ")}\n`
509
+ + `Last output:\n${tail || "(no output yet — possible npm registry or network stall; try again or run the same npm command in a terminal)"}`,
510
+ );
511
+ err.timedOut = true;
512
+ err.stdout = stdout;
513
+ err.stderr = stderr;
514
+ finish(reject, err);
515
+ }, timeoutMs);
516
+ }
517
+
518
+ if (typeof heartbeatMs === "number" && heartbeatMs > 0 && emitFn) {
519
+ const start = Date.now();
520
+ heartbeatId = setInterval(() => {
521
+ const sec = Math.floor((Date.now() - start) / 1000);
522
+ emitFn({
523
+ type: "stdout",
524
+ text: `[installer] ${heartbeatText} (${sec}s elapsed).\n`,
525
+ urls: [],
526
+ });
527
+ }, heartbeatMs);
528
+ }
529
+
433
530
  child.stdout?.on("data", (d) => {
434
531
  const text = d.toString();
435
532
  stdout += text;
@@ -443,8 +540,9 @@ function runCommandWithEvents(cmd, args = [], opts = {}) {
443
540
  });
444
541
 
445
542
  child.on("close", (code) => {
543
+ if (settled) return;
446
544
  const urls = [...new Set([...extractUrls(stdout), ...extractUrls(stderr)])];
447
- if (code === 0) resolve({ stdout, stderr, code, urls });
545
+ if (code === 0) finish(resolve, { stdout, stderr, code, urls });
448
546
  else {
449
547
  const isOom = code === 137 || (stderr || stdout || "").includes("Killed");
450
548
  const raw = (stderr || "").trim();
@@ -459,10 +557,10 @@ function runCommandWithEvents(cmd, args = [], opts = {}) {
459
557
  err.stderr = stderr;
460
558
  err.urls = urls;
461
559
  err.oom = isOom;
462
- reject(err);
560
+ finish(reject, err);
463
561
  }
464
562
  });
465
- child.on("error", reject);
563
+ child.on("error", (e) => finish(reject, e));
466
564
  });
467
565
  }
468
566
 
@@ -494,7 +592,7 @@ export async function ensureOpenClawGlobalPackageDependencies() {
494
592
  return { skipped: true, reason: "global_openclaw_dir_not_found" };
495
593
  }
496
594
  const registry = "https://registry.npmjs.org/";
497
- const installFlags = ["install", "--omit=dev", "--ignore-scripts", "--registry", registry];
595
+ const installFlags = ["install", "--omit=dev", "--ignore-scripts", "--no-audit", "--no-fund", "--registry", registry];
498
596
  await runCommandWithEvents("npm", installFlags, { cwd: dir, shell: false });
499
597
  await runCommandWithEvents(
500
598
  "npm",
@@ -503,6 +601,8 @@ export async function ensureOpenClawGlobalPackageDependencies() {
503
601
  "--omit=dev",
504
602
  "--no-save",
505
603
  "--ignore-scripts",
604
+ "--no-audit",
605
+ "--no-fund",
506
606
  "--registry",
507
607
  registry,
508
608
  "grammy",
@@ -533,11 +633,49 @@ async function installOpenClawPlatform(onEvent) {
533
633
  });
534
634
  }
535
635
  const npmCwd = getNpmGlobalInstallCwd();
536
- await runCommandWithEvents("npm", ["install", "-g", "--ignore-scripts", "--registry", "https://registry.npmjs.org/", `openclaw@${OPENCLAW_VERSION}`], {
537
- onEvent,
538
- cwd: npmCwd,
539
- shell: false,
540
- });
636
+ const npmTimeoutMs = Number.parseInt(String(process.env.TRADERCLAW_OPENCLAW_NPM_TIMEOUT_MS || "").trim(), 10);
637
+ const effectiveTimeout = Number.isFinite(npmTimeoutMs) && npmTimeoutMs > 0 ? npmTimeoutMs : 1_800_000;
638
+ if (typeof onEvent === "function") {
639
+ onEvent({
640
+ type: "stdout",
641
+ text:
642
+ `Running: npm install -g openclaw@${OPENCLAW_VERSION} (cwd=${npmCwd}, --no-audit --no-fund). `
643
+ + "First-time or upgrade installs can take several minutes; live npm lines and heartbeats appear below. "
644
+ + `Also watch the terminal where you started \`traderclaw install --wizard\`. `
645
+ + `Override stall limit: TRADERCLAW_OPENCLAW_NPM_TIMEOUT_MS (ms), default ${effectiveTimeout}.\n`,
646
+ urls: [],
647
+ });
648
+ }
649
+ await runCommandWithEvents(
650
+ "npm",
651
+ [
652
+ "install",
653
+ "-g",
654
+ "--ignore-scripts",
655
+ "--no-audit",
656
+ "--no-fund",
657
+ "--loglevel",
658
+ "info",
659
+ "--registry",
660
+ "https://registry.npmjs.org/",
661
+ `openclaw@${OPENCLAW_VERSION}`,
662
+ ],
663
+ {
664
+ onEvent,
665
+ cwd: npmCwd,
666
+ shell: false,
667
+ timeoutMs: effectiveTimeout,
668
+ heartbeatMs: 60_000,
669
+ heartbeatText:
670
+ "npm still installing OpenClaw — if this repeats for a long time, check disk space, DNS, and outbound HTTPS to registry.npmjs.org",
671
+ env: {
672
+ ...process.env,
673
+ // Non-interactive / fewer slow npm side trips (fundraising prompts, audit).
674
+ ...(process.env.CI ? {} : { CI: "true" }),
675
+ npm_config_update_notifier: process.env.npm_config_update_notifier ?? "false",
676
+ },
677
+ },
678
+ );
541
679
  const available = commandExists("openclaw");
542
680
  let version = available ? getCommandOutput("openclaw --version", { timeoutMs: OPENCLAW_CLI_VERSION_TIMEOUT_MS }) : null;
543
681
  if (available && !version && typeof onEvent === "function") {
@@ -618,7 +756,7 @@ function isNpmFilesystemPackageSpec(spec) {
618
756
  * IMPORTANT: run with `{ shell: false }` — `spawn(..., { shell: true })` can drop argv on Unix and npm then mis-resolves the package name.
619
757
  */
620
758
  function npmGlobalInstallArgs(spec, { force = false } = {}) {
621
- const args = ["install", "-g", "--ignore-scripts"];
759
+ const args = ["install", "-g", "--ignore-scripts", "--no-audit", "--no-fund"];
622
760
  if (force) args.push("--force");
623
761
  if (!isNpmFilesystemPackageSpec(spec)) {
624
762
  args.push("--registry", "https://registry.npmjs.org/");
@@ -757,6 +895,31 @@ async function installAndEnableOpenClawPlugin(modeConfig, onEvent, orchestratorU
757
895
  };
758
896
  }
759
897
 
898
+ /**
899
+ * Idempotent: ensure OpenClaw discovers skills under ~/.openclaw/extensions/<pluginId>/skills (extraDirs).
900
+ * See OpenClaw workspace skill loader: config.skills.load.extraDirs → openclaw-extra.
901
+ * @param {Record<string, unknown>} config
902
+ * @param {string} pluginId
903
+ */
904
+ function ensureTraderSkillsExtraDir(config, pluginId) {
905
+ const marker = `.openclaw/extensions/${pluginId}/skills`;
906
+ const tildeEntry = `~/.openclaw/extensions/${pluginId}/skills`;
907
+ if (!config.skills || typeof config.skills !== "object") config.skills = {};
908
+ if (!config.skills.load || typeof config.skills.load !== "object") config.skills.load = {};
909
+ const raw = config.skills.load.extraDirs;
910
+ const dirs = Array.isArray(raw) ? [...raw] : [];
911
+ const normalized = (d) => (typeof d === "string" ? d.replace(/\\/g, "/") : "");
912
+ const needle = normalized(tildeEntry);
913
+ const hasMarker = dirs.some((d) => {
914
+ const n = normalized(d);
915
+ return n.includes(marker) || n === needle;
916
+ });
917
+ if (!hasMarker) {
918
+ dirs.push(tildeEntry);
919
+ config.skills.load.extraDirs = dirs;
920
+ }
921
+ }
922
+
760
923
  function seedPluginConfig(modeConfig, orchestratorUrl, configPath = CONFIG_FILE) {
761
924
  const defaultUrl = orchestratorUrl || "https://api.traderclaw.ai";
762
925
 
@@ -794,6 +957,8 @@ function seedPluginConfig(modeConfig, orchestratorUrl, configPath = CONFIG_FILE)
794
957
 
795
958
  mergeOrchestratorForId(modeConfig.pluginId);
796
959
 
960
+ ensureTraderSkillsExtraDir(config, modeConfig.pluginId);
961
+
797
962
  // Do not set plugins.allow here: OpenClaw validates allow[] against the plugin registry, and
798
963
  // the id is not registered until after `openclaw plugins install`. Pre-seeding allow caused:
799
964
  // "plugins.allow: plugin not found: <id>".
@@ -1316,10 +1481,8 @@ function deployGatewayConfig(modeConfig) {
1316
1481
  const gatewayDir = join(CONFIG_DIR, "gateway");
1317
1482
  mkdirSync(gatewayDir, { recursive: true });
1318
1483
  const destFile = join(gatewayDir, modeConfig.gatewayConfig);
1319
- const npmRoot = getCommandOutput("npm root -g");
1320
- if (!npmRoot) return { deployed: false, dest: destFile };
1321
- const src = join(npmRoot, modeConfig.pluginPackage, "config", modeConfig.gatewayConfig);
1322
- if (!existsSync(src)) return { deployed: false, dest: destFile };
1484
+ const src = resolveGatewayConfigSourcePath(modeConfig, modeConfig.gatewayConfig);
1485
+ if (!src) return { deployed: false, dest: destFile };
1323
1486
  writeFileSync(destFile, readFileSync(src));
1324
1487
  return { deployed: true, source: src, dest: destFile };
1325
1488
  }
@@ -1356,13 +1519,13 @@ export function resolveAgentWorkspaceDir(configPath = CONFIG_FILE) {
1356
1519
  }
1357
1520
 
1358
1521
  /**
1359
- * Copy skills/solana-trader/HEARTBEAT.md from the globally installed npm package into the workspace root.
1522
+ * Copy skills/solana-trader/HEARTBEAT.md from the plugin package, OpenClaw extension, or global npm into the workspace root.
1360
1523
  * Skips overwrite if a non-empty file already exists (user may have customized it).
1361
1524
  */
1362
1525
  export function deployWorkspaceHeartbeat(modeConfig) {
1363
- const npmRoot = getCommandOutput("npm root -g");
1364
- if (!npmRoot) return { deployed: false, reason: "npm_root_g_failed" };
1365
- const src = join(npmRoot, modeConfig.pluginPackage, "skills", "solana-trader", "HEARTBEAT.md");
1526
+ const skillRoot = resolveSolanaTraderPackagedRoot(modeConfig);
1527
+ if (!skillRoot) return { deployed: false, reason: "source_missing" };
1528
+ const src = join(skillRoot, "HEARTBEAT.md");
1366
1529
  if (!existsSync(src)) return { deployed: false, reason: "source_missing", src };
1367
1530
 
1368
1531
  const workspaceDir = resolveAgentWorkspaceDir(CONFIG_FILE);
@@ -1389,10 +1552,10 @@ export function deployWorkspaceHeartbeat(modeConfig) {
1389
1552
  * Skips files that already exist and are non-empty so user customisations are preserved.
1390
1553
  */
1391
1554
  export function deployWorkspaceBootstrapFiles(modeConfig) {
1392
- const npmRoot = getCommandOutput("npm root -g");
1393
- if (!npmRoot) return { deployed: [], skipped: [], failed: [], reason: "npm_root_g_failed" };
1555
+ const skillRoot = resolveSolanaTraderPackagedRoot(modeConfig);
1556
+ if (!skillRoot) return { deployed: [], skipped: [], failed: [], reason: "source_dir_missing" };
1394
1557
 
1395
- const srcDir = join(npmRoot, modeConfig.pluginPackage, "skills", "solana-trader", "workspace");
1558
+ const srcDir = join(skillRoot, "workspace");
1396
1559
  if (!existsSync(srcDir)) return { deployed: [], skipped: [], failed: [], reason: "source_dir_missing", srcDir };
1397
1560
 
1398
1561
  const workspaceDir = resolveAgentWorkspaceDir(CONFIG_FILE);
@@ -2580,7 +2743,7 @@ export class InstallerStepEngine {
2580
2743
  }
2581
2744
  this.emitLog("install_qmd", "info", "Installing @tobilu/qmd globally for vector search memory...");
2582
2745
  try {
2583
- await runCommandWithEvents("npm", ["install", "-g", "--ignore-scripts", "--registry", "https://registry.npmjs.org/", "@tobilu/qmd"], {
2746
+ await runCommandWithEvents("npm", ["install", "-g", "--ignore-scripts", "--no-audit", "--no-fund", "--registry", "https://registry.npmjs.org/", "@tobilu/qmd"], {
2584
2747
  onEvent: (evt) => this.emitLog("install_qmd", evt.type === "stderr" ? "warn" : "info", evt.text, evt.urls || []),
2585
2748
  });
2586
2749
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "traderclaw-cli",
3
- "version": "1.0.117",
3
+ "version": "1.0.121",
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.117"
20
+ "solana-traderclaw": "^1.0.121"
21
21
  },
22
22
  "keywords": [
23
23
  "traderclaw",