Install
openclaw skills install node-transferHigh-speed, memory-efficient file transfer between OpenClaw nodes using Node.js native streams and token-secured HTTP streaming without Base64 encoding.
openclaw skills install node-transferHigh-speed, memory-efficient file transfer between OpenClaw nodes using native Node.js streams.
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 |
node-transfer uses native HTTP streaming with Node.js streams, providing:
| 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 |
┌──────────────┐ 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 │
└──────────────┘ └──────────────┘
Sender (send.js):
Receiver (receive.js):
Instead of deploying scripts on every transfer, we deploy them once per node and use a fast version check for subsequent transfers.
# Generate deployment script for a target node
node deploy.js E3V3
# This outputs a PowerShell script that you can execute via nodes.invoke()
On each target node, create the directory and copy files:
# 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
// 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 ...
}
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`);
node send.js /path/to/file.zip
Output:
{
"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:
node send.js /path/to/file.zip --port 8080 --timeout 10
node send.js --help
node send.js --version
node receive.js "http://192.168.1.10:54321/transfer" "token-here..." /path/to/save.zip
Output:
{
"success": true,
"bytesReceived": 1073741824,
"totalBytes": 1073741824,
"duration": 8.42,
"speedMBps": 121.5,
"outputPath": "/path/to/save.zip"
}
Options:
node receive.js <url> <token> <output> --timeout 60 --no-progress
node receive.js --help
node receive.js --version
Starts an HTTP server to stream a file.
Usage: node send.js <filePath> [options]
Arguments:
filePath (required): Path to the file to sendOptions:
--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):
{
"error": "ERROR_CODE",
"message": "Human-readable description"
}
Error codes: FILE_NOT_FOUND, NOT_A_FILE, SERVER_ERROR, TIMEOUT, READ_ERROR, RESPONSE_ERROR
Connects to a sender and downloads a file.
Usage: node receive.js <url> <token> <outputPath> [options]
Arguments:
url (required): URL from send.js outputtoken (required): Security token from send.js outputoutputPath (required): Path to save the received fileOptions:
--timeout <n>: Connection timeout in seconds (default: 30)--no-progress: Suppress progress updatesOutput (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):
{
"progress": true,
"receivedBytes": 536870912,
"totalBytes": 1073741824,
"percent": 50,
"speedMBps": 125.4
}
Exit Codes:
0: Success1: 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
Fast check if node-transfer is installed on a node.
Usage: node ensure-installed.js <targetDir>
Arguments:
targetDir (required): Directory to checkOutput (JSON):
Installed:
{
"installed": true,
"version": "1.0.0",
"message": "node-transfer is installed and up-to-date"
}
Needs installation:
{
"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-date1: Needs installation/update2: Error (invalid directory, etc.)Generates deployment scripts for the main agent.
Usage: node deploy.js <nodeId> [targetDir]
Output: JSON with:
script: PowerShell script to deploy filesescapedScript: Escaped version for command-line useusage: Example code for JavaScript and CLI usageCause: Network connectivity issue or firewall blocking connection.
Solutions:
--port--timeoutCause: Token mismatch or URL manipulation.
Solutions:
Cause: Multiple connections attempted with same token.
Solutions:
Cause: Invalid file path on sender.
Solutions:
Cause: Connection interrupted or network error.
Solutions:
Cause: Files were modified or corrupted.
Solutions:
Cause: Not using ensure-installed.js check pattern.
Solutions:
installed: false| 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 |
See CONTRIBUTING_PROPOSAL.md for information on how this could be integrated into OpenClaw core.
Built for OpenClaw - No Base64, No OOM, No Waiting.