@clawlabz/clawnetwork 0.1.20 → 0.1.22

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.20'
10
+ const VERSION = '0.1.22'
11
11
  const PLUGIN_ID = 'clawnetwork'
12
12
  const GITHUB_REPO = 'clawlabz/claw-network'
13
13
  const DEFAULT_RPC_PORT = 9710
@@ -484,11 +484,13 @@ interface NodeStatus {
484
484
  let nodeStartedAt: number | null = null
485
485
  let lastHealth: { blockHeight: number | null; peerCount: number | null; syncing: boolean } = { blockHeight: null, peerCount: null, syncing: false }
486
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
487
489
 
488
- function isNodeRunning(rpcPort: number = DEFAULT_RPC_PORT): { running: boolean; pid: number | null } {
489
- // Check in-memory process first
490
+ function isNodeRunning(): { running: boolean; pid: number | null } {
491
+ // 1. In-memory process reference
490
492
  if (nodeProcess && !nodeProcess.killed) return { running: true, pid: nodeProcess.pid }
491
- // Check PID file (for detached processes from previous CLI invocations)
493
+ // 2. PID file the ONLY authority for "is MY node running"
492
494
  const pidFile = path.join(WORKSPACE_DIR, 'node.pid')
493
495
  try {
494
496
  const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10)
@@ -499,27 +501,50 @@ function isNodeRunning(rpcPort: number = DEFAULT_RPC_PORT): { running: boolean;
499
501
  }
500
502
  }
501
503
  } catch { /* no file */ }
502
- // Last resort: check if RPC port is responding (covers orphaned processes)
503
- try {
504
- execFileSync('curl', ['-sf', '--max-time', '1', `http://localhost:${rpcPort}/health`], { timeout: 3000, encoding: 'utf8' })
505
- // Port is responding — find PID by port
506
- try {
507
- const lsof = execFileSync('lsof', ['-ti', `:${rpcPort}`], { timeout: 3000, encoding: 'utf8' }).trim()
508
- const pid = parseInt(lsof.split('\n')[0], 10)
509
- if (pid > 0) {
510
- // Write recovered PID to file for future management
511
- try { fs.writeFileSync(pidFile, String(pid)) } catch { /* ok */ }
512
- return { running: true, pid }
513
- }
514
- } catch { /* ok */ }
515
- return { running: true, pid: null }
516
- } catch { /* not responding */ }
504
+ // No port probing if we don't have a PID, we don't own any running node
517
505
  return { running: false, pid: null }
518
506
  }
519
507
 
508
+ /** Check if a TCP port is in use via nc -z (works across users, no bind needed) */
509
+ function isPortInUse(port: number): boolean {
510
+ try {
511
+ execFileSync('nc', ['-z', '127.0.0.1', String(port)], { timeout: 2000, stdio: 'ignore' })
512
+ return true // connection succeeded → something is listening
513
+ } catch {
514
+ return false // connection refused → port is free
515
+ }
516
+ }
517
+
518
+ /** Find available ports starting from the configured ones, skipping occupied ports */
519
+ function findAvailablePorts(rpcPort: number, p2pPort: number, api: OpenClawApi): { rpcPort: number; p2pPort: number } {
520
+ const MAX_TRIES = 20
521
+ let rpc = rpcPort
522
+ let p2p = p2pPort
523
+
524
+ // Find available RPC port
525
+ for (let i = 0; i < MAX_TRIES; i++) {
526
+ if (!isPortInUse(rpc)) break
527
+ api.logger?.info?.(`[clawnetwork] RPC port ${rpc} in use, trying ${rpc + 1}...`)
528
+ rpc++
529
+ }
530
+
531
+ // Find available P2P port
532
+ for (let i = 0; i < MAX_TRIES; i++) {
533
+ if (!isPortInUse(p2p)) break
534
+ api.logger?.info?.(`[clawnetwork] P2P port ${p2p} in use, trying ${p2p + 1}...`)
535
+ p2p++
536
+ }
537
+
538
+ if (rpc !== rpcPort || p2p !== p2pPort) {
539
+ api.logger?.info?.(`[clawnetwork] resolved ports: RPC=${rpc} (config=${rpcPort}), P2P=${p2p} (config=${p2pPort})`)
540
+ }
541
+ return { rpcPort: rpc, p2pPort: p2p }
542
+ }
543
+
520
544
  function buildStatus(cfg: PluginConfig): NodeStatus {
521
545
  const wallet = loadWallet()
522
- const nodeState = isNodeRunning(cfg.rpcPort)
546
+ const nodeState = isNodeRunning()
547
+ const rpcPort = activeRpcPort ?? cfg.rpcPort
523
548
  const uptime = nodeStartedAt ? Math.floor((Date.now() - nodeStartedAt) / 1000) : null
524
549
  return {
525
550
  running: nodeState.running,
@@ -528,7 +553,7 @@ function buildStatus(cfg: PluginConfig): NodeStatus {
528
553
  peerCount: lastHealth.peerCount,
529
554
  network: cfg.network,
530
555
  syncMode: cfg.syncMode,
531
- rpcUrl: `http://localhost:${cfg.rpcPort}`,
556
+ rpcUrl: `http://localhost:${rpcPort}`,
532
557
  walletAddress: wallet?.address ?? '',
533
558
  binaryVersion: cachedBinaryVersion,
534
559
  pluginVersion: VERSION,
@@ -569,12 +594,12 @@ async function rpcCall(rpcPort: number, method: string, params: unknown[] = []):
569
594
  }
570
595
 
571
596
  function startNodeProcess(binaryPath: string, cfg: PluginConfig, api: OpenClawApi): void {
572
- // Guard: check in-memory reference, PID file, AND health endpoint
597
+ // Guard: check in-memory reference and PID file only (not port)
573
598
  if (nodeProcess && !nodeProcess.killed) {
574
599
  api.logger?.warn?.('[clawnetwork] node already running (in-memory)')
575
600
  return
576
601
  }
577
- const existingState = isNodeRunning(cfg.rpcPort)
602
+ const existingState = isNodeRunning()
578
603
  if (existingState.running) {
579
604
  api.logger?.info?.(`[clawnetwork] node already running (pid=${existingState.pid}), skipping start`)
580
605
  return
@@ -583,7 +608,12 @@ function startNodeProcess(binaryPath: string, cfg: PluginConfig, api: OpenClawAp
583
608
  if (!isValidNetwork(cfg.network)) { api.logger?.error?.(`[clawnetwork] invalid network: ${cfg.network}`); return }
584
609
  if (!isValidSyncMode(cfg.syncMode)) { api.logger?.error?.(`[clawnetwork] invalid sync mode: ${cfg.syncMode}`); return }
585
610
 
586
- const args = ['start', '--network', cfg.network, '--rpc-port', String(cfg.rpcPort), '--p2p-port', String(cfg.p2pPort), '--sync-mode', cfg.syncMode, '--allow-genesis']
611
+ // Find available ports (auto-increment if configured ports are occupied by other processes)
612
+ const ports = findAvailablePorts(cfg.rpcPort, cfg.p2pPort, api)
613
+ activeRpcPort = ports.rpcPort
614
+ activeP2pPort = ports.p2pPort
615
+
616
+ const args = ['start', '--network', cfg.network, '--rpc-port', String(ports.rpcPort), '--p2p-port', String(ports.p2pPort), '--sync-mode', cfg.syncMode, '--allow-genesis']
587
617
 
588
618
  // Add bootstrap peers: built-in for the network + user-configured extra peers
589
619
  const peers = [...(BOOTSTRAP_PEERS[cfg.network] ?? []), ...cfg.extraBootstrapPeers]
@@ -626,6 +656,10 @@ function startNodeProcess(binaryPath: string, cfg: PluginConfig, api: OpenClawAp
626
656
  const pidFile = path.join(WORKSPACE_DIR, 'node.pid')
627
657
  fs.writeFileSync(pidFile, String(nodeProcess.pid))
628
658
 
659
+ // Save actual runtime ports to workspace (UI server and health checks read from here)
660
+ const runtimeCfg = path.join(WORKSPACE_DIR, 'runtime.json')
661
+ fs.writeFileSync(runtimeCfg, JSON.stringify({ rpcPort: ports.rpcPort, p2pPort: ports.p2pPort, pid: nodeProcess.pid, startedAt: Date.now() }))
662
+
629
663
  nodeProcess.on('exit', (code: number | null) => {
630
664
  api.logger?.warn?.(`[clawnetwork] node exited with code ${code}`)
631
665
  fs.closeSync(logFd)
@@ -654,7 +688,8 @@ function startHealthCheck(cfg: PluginConfig, api: OpenClawApi): void {
654
688
  if (healthTimer) clearTimeout(healthTimer)
655
689
 
656
690
  const check = async () => {
657
- lastHealth = await checkHealth(cfg.rpcPort)
691
+ const rpcPort = activeRpcPort ?? cfg.rpcPort
692
+ lastHealth = await checkHealth(rpcPort)
658
693
  if (lastHealth.blockHeight !== null) {
659
694
  api.logger?.info?.(`[clawnetwork] height=${lastHealth.blockHeight} peers=${lastHealth.peerCount} syncing=${lastHealth.syncing}`)
660
695
  }
@@ -673,7 +708,7 @@ function stopNode(api: OpenClawApi): void {
673
708
  healthTimer = null
674
709
  }
675
710
 
676
- // Find PID: in-memory process or PID file (for detached processes)
711
+ // Find PID: in-memory process or PID file the ONLY ways to identify our node
677
712
  let pid: number | null = nodeProcess?.pid ?? null
678
713
  const pidFile = path.join(WORKSPACE_DIR, 'node.pid')
679
714
  if (!pid) {
@@ -685,23 +720,26 @@ function stopNode(api: OpenClawApi): void {
685
720
 
686
721
  if (pid) {
687
722
  api.logger?.info?.(`[clawnetwork] stopping node pid=${pid} (SIGTERM)...`)
688
- try { process.kill(pid, 'SIGTERM') } catch { /* already dead */ }
723
+ try { process.kill(pid, 'SIGTERM') } catch (e: unknown) {
724
+ api.logger?.warn?.(`[clawnetwork] failed to kill pid=${pid}: ${(e as Error).message}`)
725
+ }
689
726
  setTimeout(() => {
690
727
  try { process.kill(pid as number, 'SIGKILL') } catch { /* ok */ }
691
728
  }, 10_000)
729
+ } else {
730
+ api.logger?.warn?.('[clawnetwork] no PID found — cannot stop node (may not be running)')
692
731
  }
693
732
 
694
733
  // Write stop signal file (tells restart loop in other CLI processes to stop)
695
734
  const stopFile = path.join(WORKSPACE_DIR, 'stop.signal')
696
735
  try { fs.writeFileSync(stopFile, String(Date.now())) } catch { /* ok */ }
697
736
 
698
- // Fallback: only use pkill if we had no PID to target (avoids killing other profiles' nodes)
699
- if (!pid) {
700
- try { execFileSync('pkill', ['-f', 'claw-node start'], { timeout: 3000 }) } catch { /* ok */ }
701
- }
737
+ // NO pkill we only kill our own process identified by PID file
702
738
 
703
739
  nodeProcess = null
704
740
  nodeStartedAt = null
741
+ activeRpcPort = null
742
+ activeP2pPort = null
705
743
  lastHealth = { blockHeight: null, peerCount: null, syncing: false }
706
744
  try { fs.unlinkSync(pidFile) } catch { /* ok */ }
707
745
  }
@@ -1716,24 +1754,16 @@ async function handle(req, res) {
1716
1754
  }
1717
1755
  if (a === 'start') {
1718
1756
  try {
1719
- // Check if already running — try RPC health first (covers stale PID file)
1757
+ // Check if already running — PID file is the only authority
1720
1758
  const pidFile = OC_PID_FILE;
1721
- try {
1722
- const h = await fetchJson('http://localhost:' + RPC_PORT + '/health');
1723
- if (h && (h.status === 'ok' || h.status === 'degraded')) {
1724
- try {
1725
- const { execSync } = require('child_process');
1726
- const pgrep = execSync("pgrep -f 'claw-node start'", { encoding: 'utf8', timeout: 3000 }).trim();
1727
- const livePid = parseInt(pgrep.split('\\n')[0], 10);
1728
- if (livePid > 0) fs.writeFileSync(pidFile, String(livePid));
1729
- } catch {}
1730
- json(200, { message: 'Node already running' }); return;
1731
- }
1732
- } catch {}
1733
- // Fallback: check PID file
1734
1759
  try {
1735
1760
  const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
1736
- if (pid > 0) { try { process.kill(pid, 0); json(200, { message: 'Node already running', pid }); return; } catch {} }
1761
+ if (pid > 0) {
1762
+ try { process.kill(pid, 0); json(200, { message: 'Node already running', pid }); return; } catch {
1763
+ // PID stale, clean up
1764
+ try { fs.unlinkSync(pidFile); } catch {}
1765
+ }
1766
+ }
1737
1767
  } catch {}
1738
1768
  // Find binary
1739
1769
  const binDir = OC_BIN_DIR;
@@ -1786,14 +1816,14 @@ async function handle(req, res) {
1786
1816
  // Write stop signal for restart loop
1787
1817
  const stopFile = OC_STOP_SIGNAL;
1788
1818
  try { fs.writeFileSync(stopFile, String(Date.now())); } catch {}
1789
- // Also kill by name (covers orphans)
1790
- try { require('child_process').execFileSync('pkill', ['-f', 'claw-node start'], { timeout: 3000 }); } catch {}
1791
- try { fs.unlinkSync(pidFile); } catch {}
1792
- // Wait for process to actually exit (max 5s)
1793
- for (let w = 0; w < 10; w++) {
1794
- try { require('child_process').execSync("pgrep -f 'claw-node start'", { timeout: 1000 }); } catch { break; }
1795
- require('child_process').execSync('sleep 0.5', { timeout: 2000 });
1819
+ // Wait for our process to exit (max 5s), using PID-specific check
1820
+ if (pid && pid > 0) {
1821
+ for (let w = 0; w < 10; w++) {
1822
+ try { process.kill(pid, 0); } catch { break; }
1823
+ require('child_process').execSync('sleep 0.5', { timeout: 2000 });
1824
+ }
1796
1825
  }
1826
+ try { fs.unlinkSync(pidFile); } catch {}
1797
1827
  json(200, { message: 'Node stopped' });
1798
1828
  } catch (e) { json(500, { error: e.message }); }
1799
1829
  return;
@@ -1808,13 +1838,17 @@ async function handle(req, res) {
1808
1838
  } catch {}
1809
1839
  const stopFile = OC_STOP_SIGNAL;
1810
1840
  try { fs.writeFileSync(stopFile, String(Date.now())); } catch {}
1811
- try { require('child_process').execFileSync('pkill', ['-f', 'claw-node start'], { timeout: 3000 }); } catch {}
1841
+ // Wait for our process to exit (PID-specific)
1842
+ try {
1843
+ const stoppedPid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
1844
+ if (stoppedPid > 0) {
1845
+ for (let w = 0; w < 10; w++) {
1846
+ try { process.kill(stoppedPid, 0); } catch { break; }
1847
+ require('child_process').execSync('sleep 0.5', { timeout: 2000 });
1848
+ }
1849
+ }
1850
+ } catch {}
1812
1851
  try { fs.unlinkSync(pidFile); } catch {}
1813
- // Wait for exit
1814
- for (let w = 0; w < 10; w++) {
1815
- try { require('child_process').execSync("pgrep -f 'claw-node start'", { timeout: 1000 }); } catch { break; }
1816
- require('child_process').execSync('sleep 0.5', { timeout: 2000 });
1817
- }
1818
1852
  try { fs.unlinkSync(stopFile); } catch {}
1819
1853
  // Now start (reuse start logic inline)
1820
1854
  const binDir = OC_BIN_DIR;
@@ -1858,7 +1892,7 @@ async function handle(req, res) {
1858
1892
  const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
1859
1893
  if (pid > 0) try { process.kill(pid, 'SIGTERM'); } catch {}
1860
1894
  } catch {}
1861
- try { require('child_process').execFileSync('pkill', ['-f', 'claw-node start'], { timeout: 5000 }); } catch {}
1895
+ // No pkill only target our own PID
1862
1896
 
1863
1897
  // 2. Download latest binary
1864
1898
  const binDir = OC_BIN_DIR;
@@ -2158,7 +2192,7 @@ export default function register(api: OpenClawApi) {
2158
2192
 
2159
2193
  const handleStart = async () => {
2160
2194
  // Check if already running (in-memory or detached via PID file)
2161
- const state = isNodeRunning(cfg.rpcPort)
2195
+ const state = isNodeRunning()
2162
2196
  if (state.running) {
2163
2197
  out({ message: 'Node already running', pid: state.pid })
2164
2198
  return
@@ -2421,7 +2455,7 @@ export default function register(api: OpenClawApi) {
2421
2455
  ;(async () => {
2422
2456
  try {
2423
2457
  // Check if already running (e.g. from a previous detached start)
2424
- const state = isNodeRunning(cfg.rpcPort)
2458
+ const state = isNodeRunning()
2425
2459
  if (state.running) {
2426
2460
  api.logger?.info?.(`[clawnetwork] node already running (pid=${state.pid})`)
2427
2461
 
@@ -2431,10 +2465,18 @@ export default function register(api: OpenClawApi) {
2431
2465
  const binary = findBinary()
2432
2466
  const localBinaryVersion = binary ? getBinaryVersion(binary) : null
2433
2467
 
2434
- // Get the RUNNING process version from health endpoint (not the file version)
2468
+ // Read actual runtime port from last run (may differ from config if port was auto-shifted)
2469
+ let runtimeRpcPort = cfg.rpcPort
2470
+ try {
2471
+ const rt = JSON.parse(fs.readFileSync(path.join(WORKSPACE_DIR, 'runtime.json'), 'utf8'))
2472
+ if (rt.rpcPort) { runtimeRpcPort = rt.rpcPort; activeRpcPort = rt.rpcPort }
2473
+ if (rt.p2pPort) { activeP2pPort = rt.p2pPort }
2474
+ } catch { /* no runtime file, use config */ }
2475
+
2476
+ // Get the RUNNING process version from health endpoint
2435
2477
  let runningProcessVersion: string | null = null
2436
2478
  try {
2437
- const health = await fetch(`http://localhost:${cfg.rpcPort}/health`)
2479
+ const health = await fetch(`http://localhost:${runtimeRpcPort}/health`)
2438
2480
  if (health.ok) {
2439
2481
  const hd = await health.json() as Record<string, unknown>
2440
2482
  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.20",
5
+ "version": "0.1.22",
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.20",
3
+ "version": "0.1.22",
4
4
  "description": "Run a ClawNetwork blockchain node inside OpenClaw. Every agent is a blockchain node.",
5
5
  "type": "module",
6
6
  "license": "MIT",