@clawlabz/clawnetwork 0.1.19 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts CHANGED
@@ -7,7 +7,7 @@ declare function setInterval(fn: () => void, ms: number): unknown
7
7
  declare function clearInterval(id: unknown): void
8
8
  declare function fetch(url: string, init?: Record<string, unknown>): Promise<{ status: number; ok: boolean; text: () => Promise<string>; json: () => Promise<unknown> }>
9
9
 
10
- const VERSION = '0.1.19'
10
+ const VERSION = '0.1.21'
11
11
  const PLUGIN_ID = 'clawnetwork'
12
12
  const GITHUB_REPO = 'clawlabz/claw-network'
13
13
  const DEFAULT_RPC_PORT = 9710
@@ -124,7 +124,10 @@ const fs = require('fs')
124
124
  const { execFileSync, spawn: nodeSpawn, fork } = require('child_process')
125
125
 
126
126
  function getBaseDir(): string {
127
- // OPENCLAW_DIR env var takes precedence (supports named profiles like ~/.openclaw-myprofile)
127
+ // Gateway sets OPENCLAW_STATE_DIR for named profiles (e.g. ~/.openclaw-ludis)
128
+ // OPENCLAW_DIR is the user-facing alias (used by install.sh)
129
+ const stateDir = process.env.OPENCLAW_STATE_DIR
130
+ if (stateDir) return stateDir
128
131
  const envDir = process.env.OPENCLAW_DIR
129
132
  if (envDir) return envDir
130
133
  return path.join(os.homedir(), '.openclaw')
@@ -481,11 +484,13 @@ interface NodeStatus {
481
484
  let nodeStartedAt: number | null = null
482
485
  let lastHealth: { blockHeight: number | null; peerCount: number | null; syncing: boolean } = { blockHeight: null, peerCount: null, syncing: false }
483
486
  let cachedBinaryVersion: string | null = null
487
+ let activeRpcPort: number | null = null // actual port the plugin node is running on (may differ from config)
488
+ let activeP2pPort: number | null = null
484
489
 
485
- function isNodeRunning(rpcPort: number = DEFAULT_RPC_PORT): { running: boolean; pid: number | null } {
486
- // Check in-memory process first
490
+ function isNodeRunning(): { running: boolean; pid: number | null } {
491
+ // 1. In-memory process reference
487
492
  if (nodeProcess && !nodeProcess.killed) return { running: true, pid: nodeProcess.pid }
488
- // Check PID file (for detached processes from previous CLI invocations)
493
+ // 2. PID file the ONLY authority for "is MY node running"
489
494
  const pidFile = path.join(WORKSPACE_DIR, 'node.pid')
490
495
  try {
491
496
  const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10)
@@ -496,27 +501,48 @@ function isNodeRunning(rpcPort: number = DEFAULT_RPC_PORT): { running: boolean;
496
501
  }
497
502
  }
498
503
  } catch { /* no file */ }
499
- // Last resort: check if RPC port is responding (covers orphaned processes)
500
- try {
501
- execFileSync('curl', ['-sf', '--max-time', '1', `http://localhost:${rpcPort}/health`], { timeout: 3000, encoding: 'utf8' })
502
- // Port is responding — find PID by port
503
- try {
504
- const lsof = execFileSync('lsof', ['-ti', `:${rpcPort}`], { timeout: 3000, encoding: 'utf8' }).trim()
505
- const pid = parseInt(lsof.split('\n')[0], 10)
506
- if (pid > 0) {
507
- // Write recovered PID to file for future management
508
- try { fs.writeFileSync(pidFile, String(pid)) } catch { /* ok */ }
509
- return { running: true, pid }
510
- }
511
- } catch { /* ok */ }
512
- return { running: true, pid: null }
513
- } catch { /* not responding */ }
504
+ // No port probing if we don't have a PID, we don't own any running node
514
505
  return { running: false, pid: null }
515
506
  }
516
507
 
508
+ /** Check if a TCP port is in use by any process */
509
+ function isPortInUse(port: number): boolean {
510
+ try {
511
+ execFileSync('lsof', ['-ti', `:${port}`], { timeout: 3000, encoding: 'utf8' })
512
+ return true
513
+ } catch { return false }
514
+ }
515
+
516
+ /** Find available ports starting from the configured ones, skipping occupied ports */
517
+ function findAvailablePorts(rpcPort: number, p2pPort: number, api: OpenClawApi): { rpcPort: number; p2pPort: number } {
518
+ const MAX_TRIES = 20
519
+ let rpc = rpcPort
520
+ let p2p = p2pPort
521
+
522
+ // Find available RPC port
523
+ for (let i = 0; i < MAX_TRIES; i++) {
524
+ if (!isPortInUse(rpc)) break
525
+ api.logger?.info?.(`[clawnetwork] RPC port ${rpc} in use, trying ${rpc + 1}...`)
526
+ rpc++
527
+ }
528
+
529
+ // Find available P2P port
530
+ for (let i = 0; i < MAX_TRIES; i++) {
531
+ if (!isPortInUse(p2p)) break
532
+ api.logger?.info?.(`[clawnetwork] P2P port ${p2p} in use, trying ${p2p + 1}...`)
533
+ p2p++
534
+ }
535
+
536
+ if (rpc !== rpcPort || p2p !== p2pPort) {
537
+ api.logger?.info?.(`[clawnetwork] resolved ports: RPC=${rpc} (config=${rpcPort}), P2P=${p2p} (config=${p2pPort})`)
538
+ }
539
+ return { rpcPort: rpc, p2pPort: p2p }
540
+ }
541
+
517
542
  function buildStatus(cfg: PluginConfig): NodeStatus {
518
543
  const wallet = loadWallet()
519
- const nodeState = isNodeRunning(cfg.rpcPort)
544
+ const nodeState = isNodeRunning()
545
+ const rpcPort = activeRpcPort ?? cfg.rpcPort
520
546
  const uptime = nodeStartedAt ? Math.floor((Date.now() - nodeStartedAt) / 1000) : null
521
547
  return {
522
548
  running: nodeState.running,
@@ -525,7 +551,7 @@ function buildStatus(cfg: PluginConfig): NodeStatus {
525
551
  peerCount: lastHealth.peerCount,
526
552
  network: cfg.network,
527
553
  syncMode: cfg.syncMode,
528
- rpcUrl: `http://localhost:${cfg.rpcPort}`,
554
+ rpcUrl: `http://localhost:${rpcPort}`,
529
555
  walletAddress: wallet?.address ?? '',
530
556
  binaryVersion: cachedBinaryVersion,
531
557
  pluginVersion: VERSION,
@@ -566,12 +592,12 @@ async function rpcCall(rpcPort: number, method: string, params: unknown[] = []):
566
592
  }
567
593
 
568
594
  function startNodeProcess(binaryPath: string, cfg: PluginConfig, api: OpenClawApi): void {
569
- // Guard: check in-memory reference, PID file, AND health endpoint
595
+ // Guard: check in-memory reference and PID file only (not port)
570
596
  if (nodeProcess && !nodeProcess.killed) {
571
597
  api.logger?.warn?.('[clawnetwork] node already running (in-memory)')
572
598
  return
573
599
  }
574
- const existingState = isNodeRunning(cfg.rpcPort)
600
+ const existingState = isNodeRunning()
575
601
  if (existingState.running) {
576
602
  api.logger?.info?.(`[clawnetwork] node already running (pid=${existingState.pid}), skipping start`)
577
603
  return
@@ -580,7 +606,12 @@ function startNodeProcess(binaryPath: string, cfg: PluginConfig, api: OpenClawAp
580
606
  if (!isValidNetwork(cfg.network)) { api.logger?.error?.(`[clawnetwork] invalid network: ${cfg.network}`); return }
581
607
  if (!isValidSyncMode(cfg.syncMode)) { api.logger?.error?.(`[clawnetwork] invalid sync mode: ${cfg.syncMode}`); return }
582
608
 
583
- const args = ['start', '--network', cfg.network, '--rpc-port', String(cfg.rpcPort), '--p2p-port', String(cfg.p2pPort), '--sync-mode', cfg.syncMode, '--allow-genesis']
609
+ // Find available ports (auto-increment if configured ports are occupied by other processes)
610
+ const ports = findAvailablePorts(cfg.rpcPort, cfg.p2pPort, api)
611
+ activeRpcPort = ports.rpcPort
612
+ activeP2pPort = ports.p2pPort
613
+
614
+ const args = ['start', '--network', cfg.network, '--rpc-port', String(ports.rpcPort), '--p2p-port', String(ports.p2pPort), '--sync-mode', cfg.syncMode, '--allow-genesis']
584
615
 
585
616
  // Add bootstrap peers: built-in for the network + user-configured extra peers
586
617
  const peers = [...(BOOTSTRAP_PEERS[cfg.network] ?? []), ...cfg.extraBootstrapPeers]
@@ -623,6 +654,10 @@ function startNodeProcess(binaryPath: string, cfg: PluginConfig, api: OpenClawAp
623
654
  const pidFile = path.join(WORKSPACE_DIR, 'node.pid')
624
655
  fs.writeFileSync(pidFile, String(nodeProcess.pid))
625
656
 
657
+ // Save actual runtime ports to workspace (UI server and health checks read from here)
658
+ const runtimeCfg = path.join(WORKSPACE_DIR, 'runtime.json')
659
+ fs.writeFileSync(runtimeCfg, JSON.stringify({ rpcPort: ports.rpcPort, p2pPort: ports.p2pPort, pid: nodeProcess.pid, startedAt: Date.now() }))
660
+
626
661
  nodeProcess.on('exit', (code: number | null) => {
627
662
  api.logger?.warn?.(`[clawnetwork] node exited with code ${code}`)
628
663
  fs.closeSync(logFd)
@@ -651,7 +686,8 @@ function startHealthCheck(cfg: PluginConfig, api: OpenClawApi): void {
651
686
  if (healthTimer) clearTimeout(healthTimer)
652
687
 
653
688
  const check = async () => {
654
- lastHealth = await checkHealth(cfg.rpcPort)
689
+ const rpcPort = activeRpcPort ?? cfg.rpcPort
690
+ lastHealth = await checkHealth(rpcPort)
655
691
  if (lastHealth.blockHeight !== null) {
656
692
  api.logger?.info?.(`[clawnetwork] height=${lastHealth.blockHeight} peers=${lastHealth.peerCount} syncing=${lastHealth.syncing}`)
657
693
  }
@@ -670,7 +706,7 @@ function stopNode(api: OpenClawApi): void {
670
706
  healthTimer = null
671
707
  }
672
708
 
673
- // Find PID: in-memory process or PID file (for detached processes)
709
+ // Find PID: in-memory process or PID file the ONLY ways to identify our node
674
710
  let pid: number | null = nodeProcess?.pid ?? null
675
711
  const pidFile = path.join(WORKSPACE_DIR, 'node.pid')
676
712
  if (!pid) {
@@ -682,23 +718,26 @@ function stopNode(api: OpenClawApi): void {
682
718
 
683
719
  if (pid) {
684
720
  api.logger?.info?.(`[clawnetwork] stopping node pid=${pid} (SIGTERM)...`)
685
- try { process.kill(pid, 'SIGTERM') } catch { /* already dead */ }
721
+ try { process.kill(pid, 'SIGTERM') } catch (e: unknown) {
722
+ api.logger?.warn?.(`[clawnetwork] failed to kill pid=${pid}: ${(e as Error).message}`)
723
+ }
686
724
  setTimeout(() => {
687
725
  try { process.kill(pid as number, 'SIGKILL') } catch { /* ok */ }
688
726
  }, 10_000)
727
+ } else {
728
+ api.logger?.warn?.('[clawnetwork] no PID found — cannot stop node (may not be running)')
689
729
  }
690
730
 
691
731
  // Write stop signal file (tells restart loop in other CLI processes to stop)
692
732
  const stopFile = path.join(WORKSPACE_DIR, 'stop.signal')
693
733
  try { fs.writeFileSync(stopFile, String(Date.now())) } catch { /* ok */ }
694
734
 
695
- // Fallback: only use pkill if we had no PID to target (avoids killing other profiles' nodes)
696
- if (!pid) {
697
- try { execFileSync('pkill', ['-f', 'claw-node start'], { timeout: 3000 }) } catch { /* ok */ }
698
- }
735
+ // NO pkill we only kill our own process identified by PID file
699
736
 
700
737
  nodeProcess = null
701
738
  nodeStartedAt = null
739
+ activeRpcPort = null
740
+ activeP2pPort = null
702
741
  lastHealth = { blockHeight: null, peerCount: null, syncing: false }
703
742
  try { fs.unlinkSync(pidFile) } catch { /* ok */ }
704
743
  }
@@ -1713,24 +1752,16 @@ async function handle(req, res) {
1713
1752
  }
1714
1753
  if (a === 'start') {
1715
1754
  try {
1716
- // Check if already running — try RPC health first (covers stale PID file)
1755
+ // Check if already running — PID file is the only authority
1717
1756
  const pidFile = OC_PID_FILE;
1718
- try {
1719
- const h = await fetchJson('http://localhost:' + RPC_PORT + '/health');
1720
- if (h && (h.status === 'ok' || h.status === 'degraded')) {
1721
- try {
1722
- const { execSync } = require('child_process');
1723
- const pgrep = execSync("pgrep -f 'claw-node start'", { encoding: 'utf8', timeout: 3000 }).trim();
1724
- const livePid = parseInt(pgrep.split('\\n')[0], 10);
1725
- if (livePid > 0) fs.writeFileSync(pidFile, String(livePid));
1726
- } catch {}
1727
- json(200, { message: 'Node already running' }); return;
1728
- }
1729
- } catch {}
1730
- // Fallback: check PID file
1731
1757
  try {
1732
1758
  const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
1733
- if (pid > 0) { try { process.kill(pid, 0); json(200, { message: 'Node already running', pid }); return; } catch {} }
1759
+ if (pid > 0) {
1760
+ try { process.kill(pid, 0); json(200, { message: 'Node already running', pid }); return; } catch {
1761
+ // PID stale, clean up
1762
+ try { fs.unlinkSync(pidFile); } catch {}
1763
+ }
1764
+ }
1734
1765
  } catch {}
1735
1766
  // Find binary
1736
1767
  const binDir = OC_BIN_DIR;
@@ -1783,14 +1814,14 @@ async function handle(req, res) {
1783
1814
  // Write stop signal for restart loop
1784
1815
  const stopFile = OC_STOP_SIGNAL;
1785
1816
  try { fs.writeFileSync(stopFile, String(Date.now())); } catch {}
1786
- // Also kill by name (covers orphans)
1787
- try { require('child_process').execFileSync('pkill', ['-f', 'claw-node start'], { timeout: 3000 }); } catch {}
1788
- try { fs.unlinkSync(pidFile); } catch {}
1789
- // Wait for process to actually exit (max 5s)
1790
- for (let w = 0; w < 10; w++) {
1791
- try { require('child_process').execSync("pgrep -f 'claw-node start'", { timeout: 1000 }); } catch { break; }
1792
- require('child_process').execSync('sleep 0.5', { timeout: 2000 });
1817
+ // Wait for our process to exit (max 5s), using PID-specific check
1818
+ if (pid && pid > 0) {
1819
+ for (let w = 0; w < 10; w++) {
1820
+ try { process.kill(pid, 0); } catch { break; }
1821
+ require('child_process').execSync('sleep 0.5', { timeout: 2000 });
1822
+ }
1793
1823
  }
1824
+ try { fs.unlinkSync(pidFile); } catch {}
1794
1825
  json(200, { message: 'Node stopped' });
1795
1826
  } catch (e) { json(500, { error: e.message }); }
1796
1827
  return;
@@ -1805,13 +1836,17 @@ async function handle(req, res) {
1805
1836
  } catch {}
1806
1837
  const stopFile = OC_STOP_SIGNAL;
1807
1838
  try { fs.writeFileSync(stopFile, String(Date.now())); } catch {}
1808
- try { require('child_process').execFileSync('pkill', ['-f', 'claw-node start'], { timeout: 3000 }); } catch {}
1839
+ // Wait for our process to exit (PID-specific)
1840
+ try {
1841
+ const stoppedPid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
1842
+ if (stoppedPid > 0) {
1843
+ for (let w = 0; w < 10; w++) {
1844
+ try { process.kill(stoppedPid, 0); } catch { break; }
1845
+ require('child_process').execSync('sleep 0.5', { timeout: 2000 });
1846
+ }
1847
+ }
1848
+ } catch {}
1809
1849
  try { fs.unlinkSync(pidFile); } catch {}
1810
- // Wait for exit
1811
- for (let w = 0; w < 10; w++) {
1812
- try { require('child_process').execSync("pgrep -f 'claw-node start'", { timeout: 1000 }); } catch { break; }
1813
- require('child_process').execSync('sleep 0.5', { timeout: 2000 });
1814
- }
1815
1850
  try { fs.unlinkSync(stopFile); } catch {}
1816
1851
  // Now start (reuse start logic inline)
1817
1852
  const binDir = OC_BIN_DIR;
@@ -1855,7 +1890,7 @@ async function handle(req, res) {
1855
1890
  const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
1856
1891
  if (pid > 0) try { process.kill(pid, 'SIGTERM'); } catch {}
1857
1892
  } catch {}
1858
- try { require('child_process').execFileSync('pkill', ['-f', 'claw-node start'], { timeout: 5000 }); } catch {}
1893
+ // No pkill only target our own PID
1859
1894
 
1860
1895
  // 2. Download latest binary
1861
1896
  const binDir = OC_BIN_DIR;
@@ -2155,7 +2190,7 @@ export default function register(api: OpenClawApi) {
2155
2190
 
2156
2191
  const handleStart = async () => {
2157
2192
  // Check if already running (in-memory or detached via PID file)
2158
- const state = isNodeRunning(cfg.rpcPort)
2193
+ const state = isNodeRunning()
2159
2194
  if (state.running) {
2160
2195
  out({ message: 'Node already running', pid: state.pid })
2161
2196
  return
@@ -2418,7 +2453,7 @@ export default function register(api: OpenClawApi) {
2418
2453
  ;(async () => {
2419
2454
  try {
2420
2455
  // Check if already running (e.g. from a previous detached start)
2421
- const state = isNodeRunning(cfg.rpcPort)
2456
+ const state = isNodeRunning()
2422
2457
  if (state.running) {
2423
2458
  api.logger?.info?.(`[clawnetwork] node already running (pid=${state.pid})`)
2424
2459
 
@@ -2428,10 +2463,18 @@ export default function register(api: OpenClawApi) {
2428
2463
  const binary = findBinary()
2429
2464
  const localBinaryVersion = binary ? getBinaryVersion(binary) : null
2430
2465
 
2431
- // Get the RUNNING process version from health endpoint (not the file version)
2466
+ // Read actual runtime port from last run (may differ from config if port was auto-shifted)
2467
+ let runtimeRpcPort = cfg.rpcPort
2468
+ try {
2469
+ const rt = JSON.parse(fs.readFileSync(path.join(WORKSPACE_DIR, 'runtime.json'), 'utf8'))
2470
+ if (rt.rpcPort) { runtimeRpcPort = rt.rpcPort; activeRpcPort = rt.rpcPort }
2471
+ if (rt.p2pPort) { activeP2pPort = rt.p2pPort }
2472
+ } catch { /* no runtime file, use config */ }
2473
+
2474
+ // Get the RUNNING process version from health endpoint
2432
2475
  let runningProcessVersion: string | null = null
2433
2476
  try {
2434
- const health = await fetch(`http://localhost:${cfg.rpcPort}/health`)
2477
+ const health = await fetch(`http://localhost:${runtimeRpcPort}/health`)
2435
2478
  if (health.ok) {
2436
2479
  const hd = await health.json() as Record<string, unknown>
2437
2480
  if (typeof hd.version === 'string') runningProcessVersion = hd.version.replace(/^v/, '')
@@ -2,7 +2,7 @@
2
2
  "id": "clawnetwork",
3
3
  "name": "ClawNetwork Node",
4
4
  "description": "Run a ClawNetwork blockchain node inside OpenClaw. Every agent is a node.",
5
- "version": "0.1.19",
5
+ "version": "0.1.21",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawlabz/clawnetwork",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "Run a ClawNetwork blockchain node inside OpenClaw. Every agent is a blockchain node.",
5
5
  "type": "module",
6
6
  "license": "MIT",