@clawlabz/clawnetwork 0.1.20 → 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.20'
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
@@ -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,48 @@ 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 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
+
520
542
  function buildStatus(cfg: PluginConfig): NodeStatus {
521
543
  const wallet = loadWallet()
522
- const nodeState = isNodeRunning(cfg.rpcPort)
544
+ const nodeState = isNodeRunning()
545
+ const rpcPort = activeRpcPort ?? cfg.rpcPort
523
546
  const uptime = nodeStartedAt ? Math.floor((Date.now() - nodeStartedAt) / 1000) : null
524
547
  return {
525
548
  running: nodeState.running,
@@ -528,7 +551,7 @@ function buildStatus(cfg: PluginConfig): NodeStatus {
528
551
  peerCount: lastHealth.peerCount,
529
552
  network: cfg.network,
530
553
  syncMode: cfg.syncMode,
531
- rpcUrl: `http://localhost:${cfg.rpcPort}`,
554
+ rpcUrl: `http://localhost:${rpcPort}`,
532
555
  walletAddress: wallet?.address ?? '',
533
556
  binaryVersion: cachedBinaryVersion,
534
557
  pluginVersion: VERSION,
@@ -569,12 +592,12 @@ async function rpcCall(rpcPort: number, method: string, params: unknown[] = []):
569
592
  }
570
593
 
571
594
  function startNodeProcess(binaryPath: string, cfg: PluginConfig, api: OpenClawApi): void {
572
- // Guard: check in-memory reference, PID file, AND health endpoint
595
+ // Guard: check in-memory reference and PID file only (not port)
573
596
  if (nodeProcess && !nodeProcess.killed) {
574
597
  api.logger?.warn?.('[clawnetwork] node already running (in-memory)')
575
598
  return
576
599
  }
577
- const existingState = isNodeRunning(cfg.rpcPort)
600
+ const existingState = isNodeRunning()
578
601
  if (existingState.running) {
579
602
  api.logger?.info?.(`[clawnetwork] node already running (pid=${existingState.pid}), skipping start`)
580
603
  return
@@ -583,7 +606,12 @@ function startNodeProcess(binaryPath: string, cfg: PluginConfig, api: OpenClawAp
583
606
  if (!isValidNetwork(cfg.network)) { api.logger?.error?.(`[clawnetwork] invalid network: ${cfg.network}`); return }
584
607
  if (!isValidSyncMode(cfg.syncMode)) { api.logger?.error?.(`[clawnetwork] invalid sync mode: ${cfg.syncMode}`); return }
585
608
 
586
- 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']
587
615
 
588
616
  // Add bootstrap peers: built-in for the network + user-configured extra peers
589
617
  const peers = [...(BOOTSTRAP_PEERS[cfg.network] ?? []), ...cfg.extraBootstrapPeers]
@@ -626,6 +654,10 @@ function startNodeProcess(binaryPath: string, cfg: PluginConfig, api: OpenClawAp
626
654
  const pidFile = path.join(WORKSPACE_DIR, 'node.pid')
627
655
  fs.writeFileSync(pidFile, String(nodeProcess.pid))
628
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
+
629
661
  nodeProcess.on('exit', (code: number | null) => {
630
662
  api.logger?.warn?.(`[clawnetwork] node exited with code ${code}`)
631
663
  fs.closeSync(logFd)
@@ -654,7 +686,8 @@ function startHealthCheck(cfg: PluginConfig, api: OpenClawApi): void {
654
686
  if (healthTimer) clearTimeout(healthTimer)
655
687
 
656
688
  const check = async () => {
657
- lastHealth = await checkHealth(cfg.rpcPort)
689
+ const rpcPort = activeRpcPort ?? cfg.rpcPort
690
+ lastHealth = await checkHealth(rpcPort)
658
691
  if (lastHealth.blockHeight !== null) {
659
692
  api.logger?.info?.(`[clawnetwork] height=${lastHealth.blockHeight} peers=${lastHealth.peerCount} syncing=${lastHealth.syncing}`)
660
693
  }
@@ -673,7 +706,7 @@ function stopNode(api: OpenClawApi): void {
673
706
  healthTimer = null
674
707
  }
675
708
 
676
- // 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
677
710
  let pid: number | null = nodeProcess?.pid ?? null
678
711
  const pidFile = path.join(WORKSPACE_DIR, 'node.pid')
679
712
  if (!pid) {
@@ -685,23 +718,26 @@ function stopNode(api: OpenClawApi): void {
685
718
 
686
719
  if (pid) {
687
720
  api.logger?.info?.(`[clawnetwork] stopping node pid=${pid} (SIGTERM)...`)
688
- 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
+ }
689
724
  setTimeout(() => {
690
725
  try { process.kill(pid as number, 'SIGKILL') } catch { /* ok */ }
691
726
  }, 10_000)
727
+ } else {
728
+ api.logger?.warn?.('[clawnetwork] no PID found — cannot stop node (may not be running)')
692
729
  }
693
730
 
694
731
  // Write stop signal file (tells restart loop in other CLI processes to stop)
695
732
  const stopFile = path.join(WORKSPACE_DIR, 'stop.signal')
696
733
  try { fs.writeFileSync(stopFile, String(Date.now())) } catch { /* ok */ }
697
734
 
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
- }
735
+ // NO pkill we only kill our own process identified by PID file
702
736
 
703
737
  nodeProcess = null
704
738
  nodeStartedAt = null
739
+ activeRpcPort = null
740
+ activeP2pPort = null
705
741
  lastHealth = { blockHeight: null, peerCount: null, syncing: false }
706
742
  try { fs.unlinkSync(pidFile) } catch { /* ok */ }
707
743
  }
@@ -1716,24 +1752,16 @@ async function handle(req, res) {
1716
1752
  }
1717
1753
  if (a === 'start') {
1718
1754
  try {
1719
- // Check if already running — try RPC health first (covers stale PID file)
1755
+ // Check if already running — PID file is the only authority
1720
1756
  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
1757
  try {
1735
1758
  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 {} }
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
+ }
1737
1765
  } catch {}
1738
1766
  // Find binary
1739
1767
  const binDir = OC_BIN_DIR;
@@ -1786,14 +1814,14 @@ async function handle(req, res) {
1786
1814
  // Write stop signal for restart loop
1787
1815
  const stopFile = OC_STOP_SIGNAL;
1788
1816
  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 });
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
+ }
1796
1823
  }
1824
+ try { fs.unlinkSync(pidFile); } catch {}
1797
1825
  json(200, { message: 'Node stopped' });
1798
1826
  } catch (e) { json(500, { error: e.message }); }
1799
1827
  return;
@@ -1808,13 +1836,17 @@ async function handle(req, res) {
1808
1836
  } catch {}
1809
1837
  const stopFile = OC_STOP_SIGNAL;
1810
1838
  try { fs.writeFileSync(stopFile, String(Date.now())); } catch {}
1811
- 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 {}
1812
1849
  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
1850
  try { fs.unlinkSync(stopFile); } catch {}
1819
1851
  // Now start (reuse start logic inline)
1820
1852
  const binDir = OC_BIN_DIR;
@@ -1858,7 +1890,7 @@ async function handle(req, res) {
1858
1890
  const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
1859
1891
  if (pid > 0) try { process.kill(pid, 'SIGTERM'); } catch {}
1860
1892
  } catch {}
1861
- try { require('child_process').execFileSync('pkill', ['-f', 'claw-node start'], { timeout: 5000 }); } catch {}
1893
+ // No pkill only target our own PID
1862
1894
 
1863
1895
  // 2. Download latest binary
1864
1896
  const binDir = OC_BIN_DIR;
@@ -2158,7 +2190,7 @@ export default function register(api: OpenClawApi) {
2158
2190
 
2159
2191
  const handleStart = async () => {
2160
2192
  // Check if already running (in-memory or detached via PID file)
2161
- const state = isNodeRunning(cfg.rpcPort)
2193
+ const state = isNodeRunning()
2162
2194
  if (state.running) {
2163
2195
  out({ message: 'Node already running', pid: state.pid })
2164
2196
  return
@@ -2421,7 +2453,7 @@ export default function register(api: OpenClawApi) {
2421
2453
  ;(async () => {
2422
2454
  try {
2423
2455
  // Check if already running (e.g. from a previous detached start)
2424
- const state = isNodeRunning(cfg.rpcPort)
2456
+ const state = isNodeRunning()
2425
2457
  if (state.running) {
2426
2458
  api.logger?.info?.(`[clawnetwork] node already running (pid=${state.pid})`)
2427
2459
 
@@ -2431,10 +2463,18 @@ export default function register(api: OpenClawApi) {
2431
2463
  const binary = findBinary()
2432
2464
  const localBinaryVersion = binary ? getBinaryVersion(binary) : null
2433
2465
 
2434
- // 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
2435
2475
  let runningProcessVersion: string | null = null
2436
2476
  try {
2437
- const health = await fetch(`http://localhost:${cfg.rpcPort}/health`)
2477
+ const health = await fetch(`http://localhost:${runtimeRpcPort}/health`)
2438
2478
  if (health.ok) {
2439
2479
  const hd = await health.json() as Record<string, unknown>
2440
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.20",
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.20",
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",