Interactive file transfer between devices on the local network using real Telegram inline keyboard buttons. Works with any device running the LocalSend app (Android, iOS, Windows, macOS, Linux).
The localsend-cli is a zero-dependency Python CLI. Install from GitHub:
curl -fsSL https://raw.githubusercontent.com/Chordlini/localsend-cli/master/localsend-cli -o ~/.local/bin/localsend-cli
chmod +x ~/.local/bin/localsend-cli
Full docs: https://github.com/Chordlini/localsend-cli
Requires Python 3.8+ and openssl (for TLS).
All menus MUST use OpenClaw's inline button format. Send buttons alongside your message using this structure:
buttons: [
[{ "text": "Label", "callback_data": "ls:action" }],
[{ "text": "Row 2", "callback_data": "ls:other" }]
]
ls: to namespace this skillcallback_data: ls:actionThis skill uses conversational state. Track where you are in the flow:
| State | Meaning | Next user input should be treated as... |
|---|---|---|
| ------- | --------- | ---------------------------------------- |
idle | No active flow | Normal message — respond normally |
awaiting_file | Asked user to drop/specify a file to send | The file to send — do NOT comment on it, describe it, or react to it. Immediately use it as the send payload. |
awaiting_text | Asked user to type text to send | The text payload — send it, don't discuss it |
awaiting_confirm | Waiting for send confirmation | Expect ls:confirm-send or ls:menu |
receiving | Receiver is active | Monitor for incoming files |
RULES:
awaiting_file state and user sends an image/file/path → treat it as the file to send. Show confirmation buttons immediately.awaiting_text state and user types anything → treat it as the text to send.awaiting_file state.idle when user taps ls:menu or the flow completes.When the user types /localsend or mentions sending/receiving files locally, send this message with real inline buttons:
Message:
📡 LocalSend — File Transfer
Buttons:
buttons: [
[
{ "text": "📤 Send", "callback_data": "ls:send" },
{ "text": "📥 Receive", "callback_data": "ls:receive" }
],
[
{ "text": "🔍 Scan Devices", "callback_data": "ls:devices" }
]
]
Do NOT run any commands yet. Wait for the button tap.
Trigger: callback_data: ls:devices or user says "scan", "discover", "find devices"
```bash
localsend-cli discover --json -t 2
```
Message:
```
📡 Found 3 devices:
```
Buttons (one device per row):
```json
buttons: [
[{ "text": "📱 Fast Potato — 192.168.0.148", "callback_data": "ls:dev:Fast Potato" }],
[{ "text": "💻 Rami-Desktop — 192.168.0.100", "callback_data": "ls:dev:Rami-Desktop" }],
[{ "text": "🖥️ Living Room PC — 192.168.0.105", "callback_data": "ls:dev:Living Room PC" }],
[
{ "text": "🔄 Refresh", "callback_data": "ls:devices" },
{ "text": "⬅️ Back", "callback_data": "ls:menu" }
]
]
```
Message:
```
📡 No devices found.
Make sure LocalSend is open on the other device and both are on the same WiFi.
```
Buttons:
```json
buttons: [
[
{ "text": "🔄 Try Again", "callback_data": "ls:devices" },
{ "text": "⬅️ Back", "callback_data": "ls:menu" }
]
]
```
callback_data: ls:dev:DEVICENAME) — store it as the selected target. Show action menu:Message:
```
✅ Selected: Fast Potato (192.168.0.148)
What do you want to do?
```
Buttons:
```json
buttons: [
[
{ "text": "📄 Send File", "callback_data": "ls:sendfile" },
{ "text": "📝 Send Text", "callback_data": "ls:sendtext" }
],
[
{ "text": "📦 Send Multiple", "callback_data": "ls:sendmulti" },
{ "text": "⬅️ Back", "callback_data": "ls:devices" }
]
]
```
Trigger: callback_data: ls:send
Run discover and show device picker (see Scan Devices flow above).
Message:
Send to Fast Potato:
Buttons:
buttons: [
[
{ "text": "📄 Send File", "callback_data": "ls:sendfile" },
{ "text": "📝 Send Text", "callback_data": "ls:sendtext" }
],
[
{ "text": "📦 Send Multiple", "callback_data": "ls:sendmulti" },
{ "text": "⬅️ Back", "callback_data": "ls:menu" }
]
]
callback_data: ls:sendfile)"Send me the file, drop a path, or tell me which file to send"stat or ls -lhMessage:
```
📤 Send to Fast Potato?
📄 project.zip — 4.2 MB
```
Buttons:
```json
buttons: [
[
{ "text": "✅ Send", "callback_data": "ls:confirm-send" },
{ "text": "❌ Cancel", "callback_data": "ls:menu" }
]
]
```
```bash
localsend-cli send --to "Fast Potato" /path/to/project.zip
```
Message:
```
✅ Sent project.zip (4.2 MB) to Fast Potato
```
Buttons:
```json
buttons: [
[
{ "text": "📤 Send Another", "callback_data": "ls:send" },
{ "text": "⬅️ Menu", "callback_data": "ls:menu" }
]
]
```
callback_data: ls:sendtext)"Type the text you want to send:"```bash
echo "user's text" > /tmp/localsend-text.txt
localsend-cli send --to "Fast Potato" /tmp/localsend-text.txt
rm /tmp/localsend-text.txt
```
Message:
```
✅ Text sent to Fast Potato
```
Buttons:
```json
buttons: [
[
{ "text": "📝 Send More Text", "callback_data": "ls:sendtext" },
{ "text": "📤 Send File", "callback_data": "ls:sendfile" }
],
[{ "text": "⬅️ Menu", "callback_data": "ls:menu" }]
]
```
callback_data: ls:sendmulti)"List the files or give me a glob pattern (e.g. ~/Screenshots/*.png)"Message:
```
📦 Send 5 files to Fast Potato?
📄 photo1.jpg — 2.1 MB
📄 photo2.jpg — 1.8 MB
📄 photo3.jpg — 3.2 MB
📄 photo4.jpg — 2.5 MB
📄 photo5.jpg — 1.9 MB
📊 Total: 11.5 MB
```
Buttons:
```json
buttons: [
[
{ "text": "✅ Send All", "callback_data": "ls:confirm-send" },
{ "text": "❌ Cancel", "callback_data": "ls:menu" }
]
]
```
```bash
localsend-cli send --to "Fast Potato" photo1.jpg photo2.jpg photo3.jpg photo4.jpg photo5.jpg
```
Message:
```
✅ Sent 5 files (11.5 MB) to Fast Potato
```
Buttons:
```json
buttons: [
[
{ "text": "📤 Send More", "callback_data": "ls:send" },
{ "text": "⬅️ Menu", "callback_data": "ls:menu" }
]
]
```
Trigger: callback_data: ls:receive or user says "receive", "start receiving", "listen"
ls -1 /home/rami/.openclaw/workspace/_incoming/ > /tmp/localsend-before.txt
localsend-cli --alias openclaw-workspace receive --save-dir /home/rami/.openclaw/workspace/_incoming/ -y
Run with run_in_background: true. Store the task ID.
CRITICAL: --alias MUST come BEFORE receive (global flag).
Message:
📡 Receiver active — "openclaw-workspace"
📁 Saving to: ~/incoming/
✅ Auto-accept: ON
Send files from your device whenever ready.
Buttons:
buttons: [
[
{ "text": "🛑 Stop", "callback_data": "ls:stop" },
{ "text": "🔄 Status", "callback_data": "ls:status" }
]
]
Poll every 3 seconds for new files:
ls -1 /home/rami/.openclaw/workspace/_incoming/ > /tmp/localsend-after.txt
diff /tmp/localsend-before.txt /tmp/localsend-after.txt
When file(s) arrive, immediately present in chat with inline buttons.
Single file:
Message:
✅ Received from Fast Potato:
📄 portfolio.zip — 240 MB
📁 Saved to: ~/incoming/portfolio.zip
Buttons (contextual by file type):
buttons: [
[
{ "text": "📂 Extract", "callback_data": "ls:extract" },
{ "text": "🚀 Deploy", "callback_data": "ls:deploy" }
],
[
{ "text": "📥 Receive More", "callback_data": "ls:receive" },
{ "text": "🛑 Stop", "callback_data": "ls:stop" }
]
]
Image file — show inline preview:
Message:
✅ Received from Fast Potato:
🖼️ screenshot.png — 2.1 MB
Include MEDIA:~/incoming/screenshot.png for inline preview.
Buttons:
buttons: [
[
{ "text": "📂 Open Folder", "callback_data": "ls:openfolder" },
{ "text": "📥 Receive More", "callback_data": "ls:receive" }
],
[{ "text": "🛑 Stop", "callback_data": "ls:stop" }]
]
Multiple files:
Message:
✅ Received 3 files from Fast Potato:
📄 app.apk — 45 MB
📄 README.md — 2 KB
🖼️ icon.png — 128 KB
📊 Total: 45.1 MB
Buttons:
buttons: [
[
{ "text": "📂 Show All", "callback_data": "ls:showall" },
{ "text": "📥 Receive More", "callback_data": "ls:receive" }
],
[{ "text": "🛑 Stop", "callback_data": "ls:stop" }]
]
Contextual button rules by file type:
.zip, .tar.gz → add 📂 Extract button.png, .jpg, .gif, .webp → show MEDIA: inline + 📂 Open Folder.apk → add 📱 Install button.html, .js, .py → add 👁️ Preview button🚀 Deploy buttonTrigger: callback_data: ls:stop
Message:
```
🛑 Receiver stopped.
```
Buttons:
```json
buttons: [
[
{ "text": "📡 Restart", "callback_data": "ls:receive" },
{ "text": "⬅️ Menu", "callback_data": "ls:menu" }
]
]
```
Trigger: callback_data: ls:status
Check if receiver is running and count new files:
ls -1 /home/rami/.openclaw/workspace/_incoming/ > /tmp/localsend-after.txt
diff /tmp/localsend-before.txt /tmp/localsend-after.txt | grep "^>" | wc -l
Message:
📡 Receiver: Running (12 min)
📁 Files received: 2
📊 Total: 242 MB
Buttons:
buttons: [
[
{ "text": "🛑 Stop", "callback_data": "ls:stop" },
{ "text": "📂 Show Files", "callback_data": "ls:showall" }
]
]
| callback_data | Action |
|---|---|
| --------------- | -------- |
ls:menu | Show main menu |
ls:send | Start send flow |
ls:receive | Start receive flow |
ls:devices | Discover devices |
ls:dev:DEVICENAME | Select a specific device |
ls:sendfile | Send single file |
ls:sendtext | Send text message |
ls:sendmulti | Send multiple files |
ls:confirm-send | Confirm and execute send |
ls:stop | Stop receiver |
ls:status | Check receiver status |
ls:extract | Extract received archive |
ls:deploy | Deploy received website |
ls:openfolder | Open save directory |
ls:showall | List all received files |
| Command | Usage |
|---|---|
| --------- | ------- |
| Discover | localsend-cli discover --json -t 2 |
| Send | localsend-cli send --to "DEVICE" file1 file2 ... |
| Receive | localsend-cli --alias NAME receive --save-dir DIR -y |
| Flag | Scope | Description |
|---|---|---|
| ------ | ------- | ------------- |
--alias NAME | Global (before subcommand) | Device name to advertise |
--to NAME | send | Target device (case-insensitive substring) |
-t N | discover | Scan duration in seconds (use 2 for speed) |
--json | discover | Machine-readable output |
--save-dir DIR | receive | Save location (default: ~/Downloads) |
-y | receive | Auto-accept transfers |
| Problem | Fix |
|---|---|
| --------- | ----- |
unrecognized arguments: --alias | Move --alias BEFORE the subcommand |
| No devices found | Open LocalSend on target, same WiFi, screen on |
| Port 53317 in use | Normal — CLI auto-falls back to 53318/53319 |
| Transfer declined (403) | Use -y on receiver side |
| Transfer hangs | Large file on slow WiFi — be patient |
references/protocol.md or https://github.com/localsend/protocol共 1 个版本