# Node Transfer

> node-transfer

- Skill: `javimosch/node-transfer` (Agent Skill)
- Install (CLI): `npx skillmds@latest add javimosch/node-transfer`
- Raw SKILL.md: https://api.skillmd.com/api/skills/javimosch/node-transfer/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: javimosch (https://skillmd.com/u/javimosch)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/javimosch/node-transfer

---

# node-transfer

High-speed, memory-efficient file transfer between OpenClaw nodes using native Node.js streams.

## 📋 Table of Contents

- [Problem Solved](#problem-solved)
- [Architecture](#architecture)
- [Requirements](#requirements)
- [Installation](#installation)
- [Usage](#usage)
- [API Reference](#api-reference)
- [Troubleshooting](#troubleshooting)

---

## 🎯 Problem Solved

### The Original Problem

When transferring large files between OpenClaw nodes using the standard `nodes.invoke` mechanism, we encountered several critical issues:

| Issue | Impact |
|-------|--------|
| **Base64 Encoding Overhead** | 33% larger payload, slower transfers |
| **Memory Exhaustion (OOM)** | Loading multi-GB files into memory crashes the process |
| **Transfer Latency** | JSON serialization/deserialization adds significant delay |
| **9-Minute Deployments** | Re-deploying scripts on every transfer |

### The Solution

`node-transfer` uses **native HTTP streaming** with Node.js streams, providing:

- ✅ **Zero memory overhead** - Files stream directly from disk to network
- ✅ **No Base64 encoding** - Raw binary transfer
- ✅ **Speed** - Line-speed limited only by network bandwidth
- ✅ **Install Once, Run Many** - Scripts persist on nodes after first deployment

### Performance Comparison

| Metric | Base64 Transfer | node-transfer | Improvement |
|--------|----------------|---------------|-------------|
| 1GB file transfer time | ~15-30 min | ~8 sec | **~150x faster** |
| Memory usage | 1GB+ | <10MB | **99% reduction** |
| First transfer overhead | N/A | ~30 sec (one-time install) | - |
| Subsequent transfers | ~15-30 min | **<1 sec** check + ~8 sec transfer | **~200x faster** |

---

## 🏗️ Architecture

### How It Works

```
┌──────────────┐     HTTP Stream      ┌──────────────┐
│  send.js     │ ◄──────────────────► │ receive.js   │
│  (Source)    │   (Token-protected)  │ (Destination)│
└──────────────┘                      └──────────────┘
       │                                     │
       ▼                                     ▼
┌──────────────┐                      ┌──────────────┐
│  Read Stream │                      │ Write Stream │
│  (fs.create  │                      │ (fs.create   │
│   ReadStream)│                      │  WriteStream)│
└──────────────┘                      └──────────────┘
       │                                     │
       ▼                                     ▼
┌──────────────┐                      ┌──────────────┐
│  File on     │                      │  File on     │
│  Disk        │                      │  Disk        │
└──────────────┘                      └──────────────┘
```

### Security Model

1. **One-time Token**: 256-bit cryptographically random token (64 hex chars)
2. **Single Connection**: Only one download allowed per token
3. **Auto-shutdown**: Server closes after transfer completes or disconnects
4. **Token Validation**: Every request must include the correct token

### Data Flow

1. **Sender** (`send.js`):
   - Generates random port and security token
   - Starts HTTP server on ephemeral port
   - Streams file directly from disk to HTTP response
   - Auto-shutdown after transfer or timeout (5 min default)

2. **Receiver** (`receive.js`):
   - Connects to sender URL with token
   - Streams HTTP response directly to disk
   - Reports progress, speed, and completion status
   - Validates received bytes match expected size

---

## 📦 Requirements

- **Node.js**: 14.0.0 or higher
- **Network**: TCP connectivity between nodes (any port 1024-65535)
- **Firewall**: Must allow outbound connections and inbound on ephemeral ports
- **Disk Space**: Sufficient space on destination for received files

---

## 🚀 Installation

### The "Install Once" Pattern

Instead of deploying scripts on every transfer, we deploy them **once per node** and use a fast version check for subsequent transfers.

### Method 1: Using deploy.js (Recommended)

```bash
# Generate deployment script for a target node
node deploy.js E3V3

# This outputs a PowerShell script that you can execute via nodes.invoke()
```

### Method 2: Manual Deployment

On each target node, create the directory and copy files:

```powershell
# Create directory
mkdir C:/openclaw/skills/node-transfer/scripts -Force

# Copy these files (ensure UTF-8 without BOM encoding):
# - send.js
# - receive.js
# - ensure-installed.js
# - version.js
```

### Method 3: Via OpenClaw Agent

```javascript
// 1. Check if already installed (< 100ms)
const check = await nodes.invoke({
    node: 'E3V3',
    command: ['node', 'C:/openclaw/skills/node-transfer/scripts/ensure-installed.js', 
              'C:/openclaw/skills/node-transfer/scripts']
});

const checkResult = JSON.parse(check.output);

if (!checkResult.installed) {
    // 2. Deploy if needed (one-time, ~30 seconds)
    // Use the deploy.js output or manually copy files
    console.log('Deploying node-transfer to E3V3...');
    // ... deployment code ...
}
```

---

## 💡 Usage

### Basic Transfer Workflow

```javascript
const INSTALL_DIR = 'C:/openclaw/skills/node-transfer/scripts';
const SOURCE_NODE = 'E3V3';
const DEST_NODE = 'E3V3-Docker';

// Step 1: Check installation on both nodes (fast!)
const [sourceCheck, destCheck] = await Promise.all([
    nodes.invoke({
        node: SOURCE_NODE,
        command: ['node', `${INSTALL_DIR}/ensure-installed.js`, INSTALL_DIR]
    }),
    nodes.invoke({
        node: DEST_NODE,
        command: ['node', `${INSTALL_DIR}/ensure-installed.js`, INSTALL_DIR]
    })
]);

// Deploy if needed (usually only once per node ever)
// ... deployment code if not installed ...

// Step 2: Start sender on source node
const sendResult = await nodes.invoke({
    node: SOURCE_NODE,
    command: ['node', `${INSTALL_DIR}/send.js`, 'C:/data/large-file.zip']
});

const { url, token, fileSize, fileName } = JSON.parse(sendResult.output);

// Step 3: Start receiver on destination node
const receiveResult = await nodes.invoke({
    node: DEST_NODE,
    command: ['node', `${INSTALL_DIR}/receive.js`, url, token, '/incoming/file.zip']
});

const result = JSON.parse(receiveResult.output);
console.log(`Transferred ${result.bytesReceived} bytes in ${result.duration}s at ${result.speedMBps} MB/s`);
```

### Using the Command Line

#### Sender

```bash
node send.js /path/to/file.zip
```

Output:
```json
{
  "url": "http://192.168.1.10:54321/transfer",
  "token": "a1b2c3d4e5f6789...",
  "fileSize": 1073741824,
  "fileName": "file.zip",
  "sourceIp": "192.168.1.10",
  "port": 54321,
  "version": "1.0.0"
}
```

Options:
```bash
node send.js /path/to/file.zip --port 8080 --timeout 10
node send.js --help
node send.js --version
```

#### Receiver

```bash
node receive.js "http://192.168.1.10:54321/transfer" "token-here..." /path/to/save.zip
```

Output:
```json
{
  "success": true,
  "bytesReceived": 1073741824,
  "totalBytes": 1073741824,
  "duration": 8.42,
  "speedMBps": 121.5,
  "outputPath": "/path/to/save.zip"
}
```

Options:
```bash
node receive.js <url> <token> <output> --timeout 60 --no-progress
node receive.js --help
node receive.js --version
```

---

## 📚 API Reference

### send.js

Starts an HTTP server to stream a file.

**Usage:** `node send.js <filePath> [options]`

**Arguments:**
- `filePath` (required): Path to the file to send

**Options:**
- `--port <n>`: Use specific port (default: random ephemeral)
- `--timeout <n>`: Timeout in minutes (default: 5)

**Output (JSON):**
| Field | Type | Description |
|-------|------|-------------|
| `url` | string | HTTP URL for receiver to connect to |
| `token` | string | Security token (64 hex chars) |
| `fileSize` | number | File size in bytes |
| `fileName` | string | Original filename |
| `sourceIp` | string | IP address of sender |
| `port` | number | TCP port used |
| `version` | string | Version of send.js |

**Exit Codes:**
- `0`: Success (transfer completed or info displayed)
- `1`: Error (check stderr for JSON error details)

**Error Output (JSON):**
```json
{
  "error": "ERROR_CODE",
  "message": "Human-readable description"
}
```

Error codes: `FILE_NOT_FOUND`, `NOT_A_FILE`, `SERVER_ERROR`, `TIMEOUT`, `READ_ERROR`, `RESPONSE_ERROR`

---

### receive.js

Connects to a sender and downloads a file.

**Usage:** `node receive.js <url> <token> <outputPath> [options]`

**Arguments:**
- `url` (required): URL from send.js output
- `token` (required): Security token from send.js output
- `outputPath` (required): Path to save the received file

**Options:**
- `--timeout <n>`: Connection timeout in seconds (default: 30)
- `--no-progress`: Suppress progress updates

**Output (JSON):**
| Field | Type | Description |
|-------|------|-------------|
| `success` | boolean | Always true on success |
| `bytesReceived` | number | Actual bytes received |
| `totalBytes` | number | Expected bytes (from Content-Length) |
| `duration` | number | Transfer time in seconds |
| `speedMBps` | number | Average speed in MB/s |
| `outputPath` | string | Absolute path to saved file |

**Progress Updates (when not using `--no-progress`):**
```json
{
  "progress": true,
  "receivedBytes": 536870912,
  "totalBytes": 1073741824,
  "percent": 50,
  "speedMBps": 125.4
}
```

**Exit Codes:**
- `0`: Success
- `1`: Error (check stderr for JSON error details)

Error codes: `INVALID_ARGS`, `INVALID_URL`, `CONNECTION_ERROR`, `HTTP_ERROR`, `TIMEOUT`, `WRITE_ERROR`, `SIZE_MISMATCH`, `FILE_EXISTS`, `NO_DATA`

---

### ensure-installed.js

Fast check if node-transfer is installed on a node.

**Usage:** `node ensure-installed.js <targetDir>`

**Arguments:**
- `targetDir` (required): Directory to check

**Output (JSON):**

Installed:
```json
{
  "installed": true,
  "version": "1.0.0",
  "message": "node-transfer is installed and up-to-date"
}
```

Needs installation:
```json
{
  "installed": false,
  "missing": ["send.js"],
  "mismatched": [],
  "currentVersion": null,
  "requiredVersion": "1.0.0",
  "action": "DEPLOY",
  "message": "Installation needed: 1 missing, 0 outdated"
}
```

**Exit Codes:**
- `0`: Already installed and up-to-date
- `1`: Needs installation/update
- `2`: Error (invalid directory, etc.)

---

### deploy.js

Generates deployment scripts for the main agent.

**Usage:** `node deploy.js <nodeId> [targetDir]`

**Output:** JSON with:
- `script`: PowerShell script to deploy files
- `escapedScript`: Escaped version for command-line use
- `usage`: Example code for JavaScript and CLI usage

---

## 🔧 Troubleshooting

### "Connection timeout"

**Cause:** Network connectivity issue or firewall blocking connection.

**Solutions:**
- Verify both nodes can reach each other
- Check firewall rules allow outbound connections
- Try specifying a specific port with `--port`
- Increase timeout with `--timeout`

### "403 Forbidden: Invalid or missing token"

**Cause:** Token mismatch or URL manipulation.

**Solutions:**
- Use the exact token from send.js output
- Don't modify the URL
- Ensure the token hasn't expired (sender times out after 5 minutes)

### "409 Conflict: Transfer already in progress"

**Cause:** Multiple connections attempted with same token.

**Solutions:**
- Each sender URL/token can only be used once
- Start a new sender if you need to retry

### "FILE_NOT_FOUND" or "NOT_A_FILE"

**Cause:** Invalid file path on sender.

**Solutions:**
- Use absolute paths
- Verify file exists
- Check file permissions

### "SIZE_MISMATCH"

**Cause:** Connection interrupted or network error.

**Solutions:**
- Retry the transfer
- Check network stability
- The partial file is automatically cleaned up

### "Hash mismatch" during ensure-installed

**Cause:** Files were modified or corrupted.

**Solutions:**
- Re-deploy scripts using deploy.js
- Ensure files are copied without modification
- Check encoding (must be UTF-8 without BOM)

### Slow transfers on subsequent runs

**Cause:** Not using `ensure-installed.js` check pattern.

**Solutions:**
- Always check installation first (< 100ms)
- Only deploy if `installed: false`
- Follow the "Install Once, Run Many" pattern

---

## 📄 Files

| File | Purpose |
|------|---------|
| `send.js` | HTTP server that streams files to receivers |
| `receive.js` | HTTP client that downloads files from senders |
| `ensure-installed.js` | Fast version/integrity check for deployment |
| `version.js` | Version manifest for update detection |
| `deploy.js` | Generates deployment scripts for agents |

---

## 🤝 Contributing

See [CONTRIBUTING_PROPOSAL.md](./CONTRIBUTING_PROPOSAL.md) for information on how this could be integrated into OpenClaw core.

---

*Built for OpenClaw - No Base64, No OOM, No Waiting.*

