feat: méthode WebSocket HA pour Lovelace + vue lumières créée

This commit is contained in:
Nox
2026-02-22 18:28:34 +00:00
parent 7d6605e33e
commit 917d1da7c0
668 changed files with 198094 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
---
name: anytype
description: Interact with Anytype knowledge base via self-hosted JSON API. Create, search, list and manage objects, spaces, types and lists.
metadata:
clawdbot:
emoji: "🧠"
---
# Anytype Integration
You have access to a self-hosted Anytype instance via its JSON API.
## Connection Details
- **Base URL**: `http://192.168.1.150:31009`
- **Auth Header**: `Authorization: Bearer VVP91vUjVAPF4xcnDZF2p61kgxu+4M04n3CGj/mngtk=`
- **Version Header**: `Anytype-Version: 2025-05-20`
Every request MUST include both headers. Use curl for all API calls.
## Common curl template
```bash
curl -s -X METHOD "http://192.168.1.150:31009/v1/ENDPOINT" \
-H "Authorization: Bearer VVP91vUjVAPF4xcnDZF2p61kgxu+4M04n3CGj/mngtk=" \
-H "Anytype-Version: 2025-05-20" \
-H "Content-Type: application/json" \
-d '{"key":"value"}'
```
## Available Endpoints
### Spaces
- **List spaces**: `GET /v1/spaces`
- **Get space**: `GET /v1/spaces/{space_id}`
- **Get space members**: `GET /v1/spaces/{space_id}/members`
### Search
- **Global search**: `POST /v1/search` with body `{"query": "search term"}`
- **Search in space**: `POST /v1/spaces/{space_id}/search` with body `{"query": "search term"}`
### Objects
- **List objects in space**: `GET /v1/spaces/{space_id}/objects`
- **Get object**: `GET /v1/spaces/{space_id}/objects/{object_id}`
- **Create object**: `POST /v1/spaces/{space_id}/objects` with body:
```json
{
"name": "Object title",
"icon": "📝",
"body": "Content in markdown format",
"type_key": "ot-page",
"description": "Optional description"
}
```
- **Update object**: `PATCH /v1/spaces/{space_id}/objects/{object_id}`
- **Delete object**: `DELETE /v1/spaces/{space_id}/objects/{object_id}`
### Types
- **List types in space**: `GET /v1/spaces/{space_id}/types`
- **Get type**: `GET /v1/spaces/{space_id}/types/{type_id}`
### Lists / Collections
- **List lists**: `GET /v1/spaces/{space_id}/lists`
- **Get list content**: `GET /v1/spaces/{space_id}/lists/{list_id}`
- **Add object to list**: `POST /v1/spaces/{space_id}/lists/{list_id}/objects` with body `{"object_id": "..."}`
### Properties / Relations
- **List properties**: `GET /v1/spaces/{space_id}/properties`
### Templates
- **List templates for type**: `GET /v1/spaces/{space_id}/types/{type_id}/templates`
## Workflow
1. **Always start by listing spaces** to get the space_id
2. Then use the space_id in subsequent calls
3. For creating objects, first list types to find the correct type_key
4. Present results in a clean, readable format to the user
5. When searching, use global search first, then refine by space if needed
## Error Handling
- 401: Token invalid or expired
- 404: Object/space not found
- 400: Bad request (check JSON body format)
- If an endpoint returns an error, show the full response to help debug
## Important Notes
- The API is RESTful with JSON responses
- All dates are in ISO 8601 format
- Object content (body) uses Markdown
- The type_key for common types: ot-page (Page), ot-task (Task), ot-note (Note), ot-bookmark (Bookmark)
- Always use `-s` flag with curl to suppress progress bars
- Pipe through `python3 -m json.tool` or `jq` for readable output when showing to user
+7
View File
@@ -0,0 +1,7 @@
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "fal-ai",
"installedVersion": "0.1.0",
"installedAt": 1771426182485
}
+33
View File
@@ -0,0 +1,33 @@
![](https://i.imgur.com/tP0xHSp.png)
# fal.ai API Skill
See [SKILL.md](./SKILL.md) for full documentation.
## Quick Start
```bash
# Set your API key
export FAL_KEY="your-api-key"
# Generate an image
python3 fal_api.py --prompt "A cute robot cat" --model flux-schnell
# List available models
python3 fal_api.py --list-models
```
## Configure Credentials
```bash
# Via environment
export FAL_KEY="your-api-key"
# Or via clawdbot config
clawdbot config set skill.fal_api.key YOUR_API_KEY
```
## Requirements
- Python 3.7+
- No external dependencies (uses stdlib)
+95
View File
@@ -0,0 +1,95 @@
---
name: fal-api
description: Generate images, videos, and audio via fal.ai API (FLUX, SDXL, Whisper, etc.)
version: 0.1.0
metadata:
{
"openclaw": { "requires": { "env": ["FAL_KEY"] }, "primaryEnv": "FAL_KEY" },
}
---
# fal.ai API Skill
Generate images, videos, and transcripts using fal.ai's API with support for FLUX, Stable Diffusion, Whisper, and more.
## Features
- Queue-based async generation (submit → poll → result)
- Support for 600+ AI models
- Image generation (FLUX, SDXL, Recraft)
- Video generation (MiniMax, WAN)
- Speech-to-text (Whisper)
- Stdlib-only dependencies (no `fal_client` required)
## Setup
1. Get your API key from https://fal.ai/dashboard/keys
2. Configure with:
```bash
export FAL_KEY="your-api-key"
```
Or via clawdbot config:
```bash
clawdbot config set skill.fal_api.key YOUR_API_KEY
```
## Usage
### Interactive Mode
```
You: Generate a cyberpunk cityscape with FLUX
Klawf: Creates the image and returns the URL
```
### Python Script
```python
from fal_api import FalAPI
api = FalAPI()
# Generate and wait
urls = api.generate_and_wait(
prompt="A serene Japanese garden",
model="flux-dev"
)
print(urls)
```
### Available Models
| Model | Endpoint | Type |
| ------------- | ------------------------------------- | ------------ |
| flux-schnell | `fal-ai/flux/schnell` | Image (fast) |
| flux-dev | `fal-ai/flux/dev` | Image |
| flux-pro | `fal-ai/flux-pro/v1.1-ultra` | Image (2K) |
| fast-sdxl | `fal-ai/fast-sdxl` | Image |
| recraft-v3 | `fal-ai/recraft-v3` | Image |
| sd35-large | `fal-ai/stable-diffusion-v35-large` | Image |
| minimax-video | `fal-ai/minimax-video/image-to-video` | Video |
| wan-video | `fal-ai/wan/v2.1/1.3b/text-to-video` | Video |
| whisper | `fal-ai/whisper` | Audio |
For the full list, run:
```bash
python3 fal_api.py --list-models
```
## Parameters
| Parameter | Type | Default | Description |
| ---------- | ---- | ---------------- | -------------------------------------------------- |
| prompt | str | required | Image/video description |
| model | str | "flux-dev" | Model name from table above |
| image_size | str | "landscape_16_9" | Preset: square, portrait_4_3, landscape_16_9, etc. |
| num_images | int | 1 | Number of images to generate |
| seed | int | None | Random seed for reproducibility |
## Credits
Built following the krea-api skill pattern. Uses fal.ai's queue-based API for reliable async generation.
+6
View File
@@ -0,0 +1,6 @@
{
"ownerId": "kn7bwx2qpadhr9wgbcqfr6q86h8098s2",
"slug": "fal-ai",
"version": "0.1.0",
"publishedAt": 1769875535293
}
+297
View File
@@ -0,0 +1,297 @@
#!/usr/bin/env python3
"""
fal.ai API - Image, Video, and Audio Generation Skill
Usage:
python fal_api.py --prompt "A beautiful sunset" --model flux-dev
Or use as a module:
from fal_api import FalAPI
api = FalAPI()
urls = api.generate_and_wait(prompt="...")
"""
import json
import os
import time
import urllib.request
import urllib.error
import argparse
from typing import Optional, List, Dict, Any
class FalAPI:
"""Client for fal.ai generative media API."""
QUEUE_URL = "https://queue.fal.run"
# Available models and their endpoints
MODELS = {
# Image generation
"flux-schnell": "fal-ai/flux/schnell",
"flux-dev": "fal-ai/flux/dev",
"flux-pro": "fal-ai/flux-pro/v1.1-ultra",
"fast-sdxl": "fal-ai/fast-sdxl",
"recraft-v3": "fal-ai/recraft-v3",
"sd35-large": "fal-ai/stable-diffusion-v35-large",
# Video generation
"minimax-video": "fal-ai/minimax-video/image-to-video",
"wan-video": "fal-ai/wan/v2.1/1.3b/text-to-video",
# Audio
"whisper": "fal-ai/whisper",
}
# Preset image sizes
IMAGE_SIZES = {
"square": "square",
"square_hd": "square_hd",
"portrait_4_3": "portrait_4_3",
"portrait_16_9": "portrait_16_9",
"landscape_4_3": "landscape_4_3",
"landscape_16_9": "landscape_16_9",
}
def __init__(self, api_key: str = None):
"""
Initialize the fal.ai API client.
Args:
api_key: Your FAL_KEY (or set via env/config)
"""
if not api_key:
api_key = os.environ.get("FAL_KEY") or self._get_config("key")
if not api_key:
raise ValueError("FAL_KEY required. Set via env or clawdbot config.")
self.api_key = api_key
self.headers = {
"Authorization": f"Key {self.api_key}",
"Content-Type": "application/json",
"Accept": "application/json",
"User-Agent": "Mozilla/5.0 (compatible; Klawf/1.0; +https://clawdhub.com/agmmnn/fal-api)"
}
def _get_config(self, key: str) -> Optional[str]:
"""Get config from clawdbot config if available."""
try:
import subprocess
result = subprocess.run(
["clawdbot", "config", "get", f"skill.fal_api.{key}"],
capture_output=True, text=True
)
return result.stdout.strip() if result.returncode == 0 else None
except Exception:
return None
def _request(self, method: str, url: str, data: dict = None) -> dict:
"""Make HTTP request to fal.ai API."""
req = urllib.request.Request(url, method=method)
for k, v in self.headers.items():
if method == "GET" and k.lower() == "content-type":
continue
req.add_header(k, v)
if data:
req.data = json.dumps(data).encode()
with urllib.request.urlopen(req, timeout=120) as response:
return json.loads(response.read().decode())
def submit(
self,
model: str,
payload: Dict[str, Any],
) -> dict:
"""
Submit a job to the queue.
Args:
model: Model name or full endpoint
payload: Request payload
Returns:
dict with request_id, status_url, response_url
"""
endpoint = self.MODELS.get(model, model)
url = f"{self.QUEUE_URL}/{endpoint}"
return self._request("POST", url, payload)
def get_status(self, model: str, request_id: str) -> dict:
"""Get the status of a queued request."""
endpoint = self.MODELS.get(model, model)
url = f"{self.QUEUE_URL}/{endpoint}/requests/{request_id}/status"
return self._request("GET", url)
def get_result(self, model: str, request_id: str) -> dict:
"""Get the result of a completed request."""
endpoint = self.MODELS.get(model, model)
url = f"{self.QUEUE_URL}/{endpoint}/requests/{request_id}"
return self._request("GET", url)
def wait_for_completion(
self,
model: str,
request_id: str,
poll_interval: float = 2.0,
timeout: float = 300.0
) -> dict:
"""Poll until job completes or times out."""
start = time.time()
while time.time() - start < timeout:
status = self.get_status(model, request_id)
state = status.get("status")
if state == "COMPLETED":
return self.get_result(model, request_id)
elif state == "FAILED":
raise Exception(f"Job failed: {status}")
time.sleep(poll_interval)
raise TimeoutError(f"Job {request_id} did not complete within {timeout}s")
def generate_image(
self,
prompt: str,
model: str = "flux-dev",
image_size: str = "landscape_16_9",
num_images: int = 1,
seed: Optional[int] = None,
**kwargs
) -> dict:
"""
Submit an image generation job.
Args:
prompt: Text description of the image
model: Model name (default: "flux-dev")
image_size: Size preset (default: "landscape_16_9")
num_images: Number of images (default: 1)
seed: Random seed for reproducibility
**kwargs: Additional model-specific parameters
Returns:
dict with request_id and status URLs
"""
payload = {
"prompt": prompt,
"image_size": self.IMAGE_SIZES.get(image_size, image_size),
"num_images": num_images,
**kwargs
}
if seed is not None:
payload["seed"] = seed
return self.submit(model, payload)
def generate_video(
self,
prompt: str,
image_url: str = None,
model: str = "minimax-video",
**kwargs
) -> dict:
"""
Submit a video generation job.
Args:
prompt: Text description
image_url: Source image URL (for image-to-video)
model: Video model name
**kwargs: Additional parameters
Returns:
dict with request_id and status URLs
"""
payload = {"prompt": prompt, **kwargs}
if image_url:
payload["image_url"] = image_url
return self.submit(model, payload)
def transcribe(
self,
audio_url: str,
model: str = "whisper",
**kwargs
) -> dict:
"""
Submit an audio transcription job.
Args:
audio_url: URL of audio file
model: Whisper model variant
**kwargs: Additional parameters
Returns:
dict with request_id and status URLs
"""
payload = {"audio_url": audio_url, **kwargs}
return self.submit(model, payload)
def generate_and_wait(
self,
prompt: str,
model: str = "flux-dev",
**kwargs
) -> List[str]:
"""Generate an image and wait for the result."""
job = self.generate_image(prompt, model, **kwargs)
request_id = job["request_id"]
print(f"Job submitted: {request_id}")
result = self.wait_for_completion(model, request_id)
# Extract URLs from result (format varies by model)
images = result.get("images", [])
if images:
return [img.get("url") for img in images if img.get("url")]
# Fallback for different response formats
if "image" in result:
return [result["image"].get("url")]
return []
def main():
parser = argparse.ArgumentParser(description="Generate media with fal.ai API")
parser.add_argument("--prompt", help="Text description")
parser.add_argument("--model", default="flux-dev", help="Model name (default: flux-dev)")
parser.add_argument("--size", default="landscape_16_9", help="Image size preset")
parser.add_argument("--num-images", type=int, default=1, help="Number of images")
parser.add_argument("--seed", type=int, help="Random seed")
parser.add_argument("--list-models", action="store_true", help="List available models")
parser.add_argument("--api-key", help="FAL_KEY (or set via environment)")
args = parser.parse_args()
if args.list_models:
print("Available models:")
for name, endpoint in FalAPI.MODELS.items():
print(f" {name:20}{endpoint}")
return
if not args.prompt:
parser.error("--prompt is required unless --list-models is set")
api = FalAPI(api_key=args.api_key)
print(f"Generating '{args.prompt[:50]}...' with {args.model}...")
urls = api.generate_and_wait(
prompt=args.prompt,
model=args.model,
image_size=args.size,
num_images=args.num_images,
seed=args.seed
)
print("\nGenerated images:")
for url in urls:
print(f" {url}")
if __name__ == "__main__":
main()
@@ -0,0 +1,7 @@
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "home-assistant",
"installedVersion": "1.0.0",
"installedAt": 1771439530092
}
+175
View File
@@ -0,0 +1,175 @@
---
name: home-assistant
description: Control Home Assistant smart home devices, run automations, and receive webhook events. Use when controlling lights, switches, climate, scenes, scripts, or any HA entity. Supports bidirectional communication via REST API (outbound) and webhooks (inbound triggers from HA automations).
metadata: {"clawdbot":{"emoji":"🏠","requires":{"bins":["jq","curl"]}}}
---
# Home Assistant
Control your smart home via Home Assistant's REST API and webhooks.
## Setup
### Option 1: Config File (Recommended)
Create `~/.config/home-assistant/config.json`:
```json
{
"url": "https://your-ha-instance.duckdns.org",
"token": "your-long-lived-access-token"
}
```
### Option 2: Environment Variables
```bash
export HA_URL="http://homeassistant.local:8123"
export HA_TOKEN="your-long-lived-access-token"
```
### Getting a Long-Lived Access Token
1. Open Home Assistant → Profile (bottom left)
2. Scroll to "Long-Lived Access Tokens"
3. Click "Create Token", name it (e.g., "Clawdbot")
4. Copy the token immediately (shown only once)
## Quick Reference
### List Entities
```bash
curl -s -H "Authorization: Bearer $HA_TOKEN" "$HA_URL/api/states" | jq '.[].entity_id'
```
### Get Entity State
```bash
curl -s -H "Authorization: Bearer $HA_TOKEN" "$HA_URL/api/states/light.living_room"
```
### Control Devices
```bash
# Turn on
curl -X POST -H "Authorization: Bearer $HA_TOKEN" -H "Content-Type: application/json" \
"$HA_URL/api/services/light/turn_on" -d '{"entity_id": "light.living_room"}'
# Turn off
curl -X POST -H "Authorization: Bearer $HA_TOKEN" -H "Content-Type: application/json" \
"$HA_URL/api/services/light/turn_off" -d '{"entity_id": "light.living_room"}'
# Set brightness (0-255)
curl -X POST -H "Authorization: Bearer $HA_TOKEN" -H "Content-Type: application/json" \
"$HA_URL/api/services/light/turn_on" -d '{"entity_id": "light.living_room", "brightness": 128}'
```
### Run Scripts & Automations
```bash
# Trigger script
curl -X POST -H "Authorization: Bearer $HA_TOKEN" "$HA_URL/api/services/script/turn_on" \
-H "Content-Type: application/json" -d '{"entity_id": "script.goodnight"}'
# Trigger automation
curl -X POST -H "Authorization: Bearer $HA_TOKEN" "$HA_URL/api/services/automation/trigger" \
-H "Content-Type: application/json" -d '{"entity_id": "automation.motion_lights"}'
```
### Activate Scenes
```bash
curl -X POST -H "Authorization: Bearer $HA_TOKEN" "$HA_URL/api/services/scene/turn_on" \
-H "Content-Type: application/json" -d '{"entity_id": "scene.movie_night"}'
```
## Common Services
| Domain | Service | Example entity_id |
|--------|---------|-------------------|
| `light` | `turn_on`, `turn_off`, `toggle` | `light.kitchen` |
| `switch` | `turn_on`, `turn_off`, `toggle` | `switch.fan` |
| `climate` | `set_temperature`, `set_hvac_mode` | `climate.thermostat` |
| `cover` | `open_cover`, `close_cover`, `stop_cover` | `cover.garage` |
| `media_player` | `play_media`, `media_pause`, `volume_set` | `media_player.tv` |
| `scene` | `turn_on` | `scene.relax` |
| `script` | `turn_on` | `script.welcome_home` |
| `automation` | `trigger`, `turn_on`, `turn_off` | `automation.sunrise` |
## Inbound Webhooks (HA → Clawdbot)
To receive events from Home Assistant automations:
### 1. Create HA Automation with Webhook Action
```yaml
# In HA automation
action:
- service: rest_command.notify_clawdbot
data:
event: motion_detected
area: living_room
```
### 2. Define REST Command in HA
```yaml
# configuration.yaml
rest_command:
notify_clawdbot:
url: "https://your-clawdbot-url/webhook/home-assistant"
method: POST
headers:
Authorization: "Bearer {{ webhook_secret }}"
Content-Type: "application/json"
payload: '{"event": "{{ event }}", "area": "{{ area }}"}'
```
### 3. Handle in Clawdbot
Clawdbot receives the webhook and can notify you or take action based on the event.
## CLI Wrapper
The `scripts/ha.sh` CLI provides easy access to all HA functions:
```bash
# Test connection
ha.sh info
# List entities
ha.sh list all # all entities
ha.sh list lights # just lights
ha.sh list switch # just switches
# Search entities
ha.sh search kitchen # find entities by name
# Get/set state
ha.sh state light.living_room
ha.sh states light.living_room # full details with attributes
ha.sh on light.living_room
ha.sh on light.living_room 200 # with brightness (0-255)
ha.sh off light.living_room
ha.sh toggle switch.fan
# Scenes & scripts
ha.sh scene movie_night
ha.sh script goodnight
# Climate
ha.sh climate climate.thermostat 22
# Call any service
ha.sh call light turn_on '{"entity_id":"light.room","brightness":200}'
```
## Troubleshooting
- **401 Unauthorized**: Token expired or invalid. Generate a new one.
- **Connection refused**: Check HA_URL, ensure HA is running and accessible.
- **Entity not found**: List entities to find the correct entity_id.
## API Reference
For advanced usage, see [references/api.md](references/api.md).
+6
View File
@@ -0,0 +1,6 @@
{
"ownerId": "kn739j7n05ptqcedg52zgnhrfh7zx24g",
"slug": "home-assistant",
"version": "1.0.0",
"publishedAt": 1769638965135
}
+175
View File
@@ -0,0 +1,175 @@
# Home Assistant REST API Reference
## Authentication
All requests require the `Authorization` header:
```
Authorization: Bearer <long-lived-access-token>
```
## Base Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/` | GET | API status and HA version |
| `/api/config` | GET | Current configuration |
| `/api/states` | GET | All entity states |
| `/api/states/<entity_id>` | GET | Single entity state |
| `/api/states/<entity_id>` | POST | Set entity state |
| `/api/services` | GET | Available services |
| `/api/services/<domain>/<service>` | POST | Call a service |
| `/api/events` | GET | Available events |
| `/api/events/<event_type>` | POST | Fire an event |
| `/api/history/period/<timestamp>` | GET | State history |
| `/api/logbook/<timestamp>` | GET | Logbook entries |
## Common Services
### Lights
```bash
# Turn on with options
POST /api/services/light/turn_on
{
"entity_id": "light.living_room",
"brightness": 255, # 0-255
"color_temp": 370, # Mireds (153-500 typically)
"rgb_color": [255, 0, 0], # RGB array
"transition": 2 # Seconds
}
# Turn off
POST /api/services/light/turn_off
{"entity_id": "light.living_room"}
```
### Climate
```bash
# Set temperature
POST /api/services/climate/set_temperature
{
"entity_id": "climate.thermostat",
"temperature": 22,
"hvac_mode": "heat" # heat, cool, auto, off
}
# Set preset
POST /api/services/climate/set_preset_mode
{
"entity_id": "climate.thermostat",
"preset_mode": "away"
}
```
### Media Player
```bash
# Play/pause
POST /api/services/media_player/media_play_pause
{"entity_id": "media_player.tv"}
# Set volume (0.0-1.0)
POST /api/services/media_player/volume_set
{"entity_id": "media_player.tv", "volume_level": 0.5}
# Play media
POST /api/services/media_player/play_media
{
"entity_id": "media_player.tv",
"media_content_type": "music",
"media_content_id": "spotify:playlist:xyz"
}
```
### Cover (Blinds/Garage)
```bash
POST /api/services/cover/open_cover
{"entity_id": "cover.garage"}
POST /api/services/cover/close_cover
{"entity_id": "cover.garage"}
POST /api/services/cover/set_cover_position
{"entity_id": "cover.blinds", "position": 50} # 0=closed, 100=open
```
### Notifications
```bash
POST /api/services/notify/mobile_app_phone
{
"message": "Motion detected!",
"title": "Security Alert",
"data": {
"image": "/local/camera_snapshot.jpg"
}
}
```
## Entity State Object
```json
{
"entity_id": "light.living_room",
"state": "on",
"attributes": {
"brightness": 255,
"color_temp": 370,
"friendly_name": "Living Room Light",
"supported_features": 63
},
"last_changed": "2024-01-15T10:30:00+00:00",
"last_updated": "2024-01-15T10:30:00+00:00"
}
```
## Webhooks (Inbound to HA)
Trigger automations via webhook:
```bash
POST /api/webhook/<webhook_id>
{"custom": "data"}
```
Create webhook trigger in automation:
```yaml
automation:
trigger:
- platform: webhook
webhook_id: my_webhook_id
allowed_methods:
- POST
```
## WebSocket API
For real-time updates, use the WebSocket API at `ws://ha-url/api/websocket`.
Connection flow:
1. Connect to WebSocket
2. Receive `auth_required`
3. Send `{"type": "auth", "access_token": "TOKEN"}`
4. Receive `auth_ok`
5. Subscribe to events: `{"id": 1, "type": "subscribe_events", "event_type": "state_changed"}`
## Error Responses
| Code | Meaning |
|------|---------|
| 400 | Bad request (invalid JSON or missing fields) |
| 401 | Unauthorized (invalid/missing token) |
| 404 | Entity or service not found |
| 405 | Method not allowed |
## Rate Limits
Home Assistant doesn't enforce strict rate limits, but avoid:
- Polling faster than every 1 second
- Bulk updates without batching
Use WebSocket for real-time state tracking instead of polling.
+172
View File
@@ -0,0 +1,172 @@
#!/usr/bin/env bash
# Home Assistant CLI wrapper
# Usage: ha.sh <command> [args...]
set -euo pipefail
CONFIG_FILE="${HA_CONFIG:-$HOME/.config/home-assistant/config.json}"
# Load config
if [[ -f "$CONFIG_FILE" ]]; then
HA_URL="${HA_URL:-$(jq -r '.url // empty' "$CONFIG_FILE")}"
HA_TOKEN="${HA_TOKEN:-$(jq -r '.token // empty' "$CONFIG_FILE")}"
fi
: "${HA_URL:?Set HA_URL or configure $CONFIG_FILE}"
: "${HA_TOKEN:?Set HA_TOKEN or configure $CONFIG_FILE}"
cmd="${1:-help}"
shift || true
api() {
curl -s -H "Authorization: Bearer $HA_TOKEN" -H "Content-Type: application/json" "$@"
}
case "$cmd" in
state|get)
# Get entity state: ha.sh state light.living_room
entity="${1:?Usage: ha.sh state <entity_id>}"
api "$HA_URL/api/states/$entity" | jq -r '.state // "unknown"'
;;
states)
# Get full entity state with attributes
entity="${1:?Usage: ha.sh states <entity_id>}"
api "$HA_URL/api/states/$entity" | jq
;;
on|turn_on)
# Turn on entity: ha.sh on light.living_room [brightness]
entity="${1:?Usage: ha.sh on <entity_id> [brightness]}"
domain="${entity%%.*}"
brightness="${2:-}"
if [[ -n "$brightness" ]]; then
api -X POST "$HA_URL/api/services/$domain/turn_on" \
-d "{\"entity_id\": \"$entity\", \"brightness\": $brightness}"
else
api -X POST "$HA_URL/api/services/$domain/turn_on" \
-d "{\"entity_id\": \"$entity\"}"
fi
echo "$entity turned on"
;;
off|turn_off)
# Turn off entity: ha.sh off light.living_room
entity="${1:?Usage: ha.sh off <entity_id>}"
domain="${entity%%.*}"
api -X POST "$HA_URL/api/services/$domain/turn_off" \
-d "{\"entity_id\": \"$entity\"}" >/dev/null
echo "$entity turned off"
;;
toggle)
# Toggle entity: ha.sh toggle switch.fan
entity="${1:?Usage: ha.sh toggle <entity_id>}"
domain="${entity%%.*}"
api -X POST "$HA_URL/api/services/$domain/toggle" \
-d "{\"entity_id\": \"$entity\"}" >/dev/null
echo "$entity toggled"
;;
scene)
# Activate scene: ha.sh scene movie_night
scene="${1:?Usage: ha.sh scene <scene_name>}"
[[ "$scene" == scene.* ]] || scene="scene.$scene"
api -X POST "$HA_URL/api/services/scene/turn_on" \
-d "{\"entity_id\": \"$scene\"}" >/dev/null
echo "✓ Scene $scene activated"
;;
script)
# Run script: ha.sh script goodnight
script="${1:?Usage: ha.sh script <script_name>}"
[[ "$script" == script.* ]] || script="script.$script"
api -X POST "$HA_URL/api/services/script/turn_on" \
-d "{\"entity_id\": \"$script\"}" >/dev/null
echo "✓ Script $script executed"
;;
automation|trigger)
# Trigger automation: ha.sh automation motion_lights
auto="${1:?Usage: ha.sh automation <automation_name>}"
[[ "$auto" == automation.* ]] || auto="automation.$auto"
api -X POST "$HA_URL/api/services/automation/trigger" \
-d "{\"entity_id\": \"$auto\"}" >/dev/null
echo "✓ Automation $auto triggered"
;;
climate|temp)
# Set temperature: ha.sh climate climate.thermostat 22
entity="${1:?Usage: ha.sh climate <entity_id> <temperature>}"
temp="${2:?Usage: ha.sh climate <entity_id> <temperature>}"
api -X POST "$HA_URL/api/services/climate/set_temperature" \
-d "{\"entity_id\": \"$entity\", \"temperature\": $temp}" >/dev/null
echo "$entity set to ${temp}°"
;;
list)
# List entities by domain: ha.sh list lights / ha.sh list all
filter="${1:-all}"
if [[ "$filter" == "all" ]]; then
api "$HA_URL/api/states" | jq -r '.[].entity_id' | sort
else
# Normalize: "lights" -> "light", "switches" -> "switch"
filter="${filter%s}"
api "$HA_URL/api/states" | jq -r --arg d "$filter" \
'.[] | select(.entity_id | startswith($d + ".")) | .entity_id' | sort
fi
;;
search)
# Search entities: ha.sh search kitchen
pattern="${1:?Usage: ha.sh search <pattern>}"
api "$HA_URL/api/states" | jq -r --arg p "$pattern" \
'.[] | select(.entity_id | test($p; "i")) | "\(.entity_id): \(.state)"'
;;
call)
# Call any service: ha.sh call light turn_on '{"entity_id":"light.room","brightness":200}'
domain="${1:?Usage: ha.sh call <domain> <service> [json_data]}"
service="${2:?Usage: ha.sh call <domain> <service> [json_data]}"
data="${3:-{}}"
api -X POST "$HA_URL/api/services/$domain/$service" -d "$data"
;;
info)
# Get HA instance info
api "$HA_URL/api/" | jq
;;
help|*)
cat <<EOF
Home Assistant CLI
Usage: ha.sh <command> [args...]
Commands:
state <entity> Get entity state
states <entity> Get full entity state with attributes
on <entity> [brightness] Turn on (optional brightness 0-255)
off <entity> Turn off
toggle <entity> Toggle on/off
scene <name> Activate scene
script <name> Run script
automation <name> Trigger automation
climate <entity> <temp> Set temperature
list [domain] List entities (lights, switches, all)
search <pattern> Search entities by name
call <domain> <svc> [json] Call any service
info Get HA instance info
Environment:
HA_URL Home Assistant URL (required)
HA_TOKEN Long-lived access token (required)
Examples:
ha.sh on light.living_room 200
ha.sh scene movie_night
ha.sh list lights
ha.sh search kitchen
EOF
;;
esac
@@ -0,0 +1,7 @@
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "nano-banana-pro",
"installedVersion": "1.0.1",
"installedAt": 1771501119947
}
+130
View File
@@ -0,0 +1,130 @@
---
name: nano-banana-pro
description: Generate/edit images with Nano Banana Pro (Gemini 3 Pro Image). Use for image create/modify requests incl. edits. Supports text-to-image + image-to-image; 1K/2K/4K; use --input-image.
---
# Nano Banana Pro Image Generation & Editing
Generate new images or edit existing ones using Google's Nano Banana Pro API (Gemini 3 Pro Image).
## Usage
Run the script using absolute path (do NOT cd to skill directory first):
**Generate new image:**
```bash
uv run ~/.codex/skills/nano-banana-pro/scripts/generate_image.py --prompt "your image description" --filename "output-name.png" [--resolution 1K|2K|4K] [--api-key KEY]
```
**Edit existing image:**
```bash
uv run ~/.codex/skills/nano-banana-pro/scripts/generate_image.py --prompt "editing instructions" --filename "output-name.png" --input-image "path/to/input.png" [--resolution 1K|2K|4K] [--api-key KEY]
```
**Important:** Always run from the user's current working directory so images are saved where the user is working, not in the skill directory.
## Default Workflow (draft → iterate → final)
Goal: fast iteration without burning time on 4K until the prompt is correct.
- Draft (1K): quick feedback loop
- `uv run ~/.codex/skills/nano-banana-pro/scripts/generate_image.py --prompt "<draft prompt>" --filename "yyyy-mm-dd-hh-mm-ss-draft.png" --resolution 1K`
- Iterate: adjust prompt in small diffs; keep filename new per run
- If editing: keep the same `--input-image` for every iteration until youre happy.
- Final (4K): only when prompt is locked
- `uv run ~/.codex/skills/nano-banana-pro/scripts/generate_image.py --prompt "<final prompt>" --filename "yyyy-mm-dd-hh-mm-ss-final.png" --resolution 4K`
## Resolution Options
The Gemini 3 Pro Image API supports three resolutions (uppercase K required):
- **1K** (default) - ~1024px resolution
- **2K** - ~2048px resolution
- **4K** - ~4096px resolution
Map user requests to API parameters:
- No mention of resolution → `1K`
- "low resolution", "1080", "1080p", "1K" → `1K`
- "2K", "2048", "normal", "medium resolution" → `2K`
- "high resolution", "high-res", "hi-res", "4K", "ultra" → `4K`
## API Key
The script checks for API key in this order:
1. `--api-key` argument (use if user provided key in chat)
2. `GEMINI_API_KEY` environment variable
If neither is available, the script exits with an error message.
## Preflight + Common Failures (fast fixes)
- Preflight:
- `command -v uv` (must exist)
- `test -n \"$GEMINI_API_KEY\"` (or pass `--api-key`)
- If editing: `test -f \"path/to/input.png\"`
- Common failures:
- `Error: No API key provided.` → set `GEMINI_API_KEY` or pass `--api-key`
- `Error loading input image:` → wrong path / unreadable file; verify `--input-image` points to a real image
- “quota/permission/403” style API errors → wrong key, no access, or quota exceeded; try a different key/account
## Filename Generation
Generate filenames with the pattern: `yyyy-mm-dd-hh-mm-ss-name.png`
**Format:** `{timestamp}-{descriptive-name}.png`
- Timestamp: Current date/time in format `yyyy-mm-dd-hh-mm-ss` (24-hour format)
- Name: Descriptive lowercase text with hyphens
- Keep the descriptive part concise (1-5 words typically)
- Use context from user's prompt or conversation
- If unclear, use random identifier (e.g., `x9k2`, `a7b3`)
Examples:
- Prompt "A serene Japanese garden" → `2025-11-23-14-23-05-japanese-garden.png`
- Prompt "sunset over mountains" → `2025-11-23-15-30-12-sunset-mountains.png`
- Prompt "create an image of a robot" → `2025-11-23-16-45-33-robot.png`
- Unclear context → `2025-11-23-17-12-48-x9k2.png`
## Image Editing
When the user wants to modify an existing image:
1. Check if they provide an image path or reference an image in the current directory
2. Use `--input-image` parameter with the path to the image
3. The prompt should contain editing instructions (e.g., "make the sky more dramatic", "remove the person", "change to cartoon style")
4. Common editing tasks: add/remove elements, change style, adjust colors, blur background, etc.
## Prompt Handling
**For generation:** Pass user's image description as-is to `--prompt`. Only rework if clearly insufficient.
**For editing:** Pass editing instructions in `--prompt` (e.g., "add a rainbow in the sky", "make it look like a watercolor painting")
Preserve user's creative intent in both cases.
## Prompt Templates (high hit-rate)
Use templates when the user is vague or when edits must be precise.
- Generation template:
- “Create an image of: <subject>. Style: <style>. Composition: <camera/shot>. Lighting: <lighting>. Background: <background>. Color palette: <palette>. Avoid: <list>.”
- Editing template (preserve everything else):
- “Change ONLY: <single change>. Keep identical: subject, composition/crop, pose, lighting, color palette, background, text, and overall style. Do not add new objects. If text exists, keep it unchanged.”
## Output
- Saves PNG to current directory (or specified path if filename includes directory)
- Script outputs the full path to the generated image
- **Do not read the image back** - just inform the user of the saved path
## Examples
**Generate new image:**
```bash
uv run ~/.codex/skills/nano-banana-pro/scripts/generate_image.py --prompt "A serene Japanese garden with cherry blossoms" --filename "2025-11-23-14-23-05-japanese-garden.png" --resolution 4K
```
**Edit existing image:**
```bash
uv run ~/.codex/skills/nano-banana-pro/scripts/generate_image.py --prompt "make the sky more dramatic with storm clouds" --filename "2025-11-23-14-25-30-dramatic-sky.png" --input-image "original-photo.jpg" --resolution 2K
```
+6
View File
@@ -0,0 +1,6 @@
{
"ownerId": "kn70pywhg0fyz996kpa8xj89s57yhv26",
"slug": "nano-banana-pro",
"version": "1.0.1",
"publishedAt": 1767651987917
}
@@ -0,0 +1,167 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "google-genai>=1.0.0",
# "pillow>=10.0.0",
# ]
# ///
"""
Generate images using Google's Nano Banana Pro (Gemini 3 Pro Image) API.
Usage:
uv run generate_image.py --prompt "your image description" --filename "output.png" [--resolution 1K|2K|4K] [--api-key KEY]
"""
import argparse
import os
import sys
from pathlib import Path
def get_api_key(provided_key: str | None) -> str | None:
"""Get API key from argument first, then environment."""
if provided_key:
return provided_key
return os.environ.get("GEMINI_API_KEY")
def main():
parser = argparse.ArgumentParser(
description="Generate images using Nano Banana Pro (Gemini 3 Pro Image)"
)
parser.add_argument(
"--prompt", "-p",
required=True,
help="Image description/prompt"
)
parser.add_argument(
"--filename", "-f",
required=True,
help="Output filename (e.g., sunset-mountains.png)"
)
parser.add_argument(
"--input-image", "-i",
help="Optional input image path for editing/modification"
)
parser.add_argument(
"--resolution", "-r",
choices=["1K", "2K", "4K"],
default="1K",
help="Output resolution: 1K (default), 2K, or 4K"
)
parser.add_argument(
"--api-key", "-k",
help="Gemini API key (overrides GEMINI_API_KEY env var)"
)
args = parser.parse_args()
# Get API key
api_key = get_api_key(args.api_key)
if not api_key:
print("Error: No API key provided.", file=sys.stderr)
print("Please either:", file=sys.stderr)
print(" 1. Provide --api-key argument", file=sys.stderr)
print(" 2. Set GEMINI_API_KEY environment variable", file=sys.stderr)
sys.exit(1)
# Import here after checking API key to avoid slow import on error
from google import genai
from google.genai import types
from PIL import Image as PILImage
# Initialise client
client = genai.Client(api_key=api_key)
# Set up output path
output_path = Path(args.filename)
output_path.parent.mkdir(parents=True, exist_ok=True)
# Load input image if provided
input_image = None
output_resolution = args.resolution
if args.input_image:
try:
input_image = PILImage.open(args.input_image)
print(f"Loaded input image: {args.input_image}")
# Auto-detect resolution if not explicitly set by user
if args.resolution == "1K": # Default value
# Map input image size to resolution
width, height = input_image.size
max_dim = max(width, height)
if max_dim >= 3000:
output_resolution = "4K"
elif max_dim >= 1500:
output_resolution = "2K"
else:
output_resolution = "1K"
print(f"Auto-detected resolution: {output_resolution} (from input {width}x{height})")
except Exception as e:
print(f"Error loading input image: {e}", file=sys.stderr)
sys.exit(1)
# Build contents (image first if editing, prompt only if generating)
if input_image:
contents = [input_image, args.prompt]
print(f"Editing image with resolution {output_resolution}...")
else:
contents = args.prompt
print(f"Generating image with resolution {output_resolution}...")
try:
response = client.models.generate_content(
model="gemini-3-pro-image-preview",
contents=contents,
config=types.GenerateContentConfig(
response_modalities=["TEXT", "IMAGE"],
image_config=types.ImageConfig(
image_size=output_resolution
)
)
)
# Process response and convert to PNG
image_saved = False
for part in response.parts:
if part.text is not None:
print(f"Model response: {part.text}")
elif part.inline_data is not None:
# Convert inline data to PIL Image and save as PNG
from io import BytesIO
# inline_data.data is already bytes, not base64
image_data = part.inline_data.data
if isinstance(image_data, str):
# If it's a string, it might be base64
import base64
image_data = base64.b64decode(image_data)
image = PILImage.open(BytesIO(image_data))
# Ensure RGB mode for PNG (convert RGBA to RGB with white background if needed)
if image.mode == 'RGBA':
rgb_image = PILImage.new('RGB', image.size, (255, 255, 255))
rgb_image.paste(image, mask=image.split()[3])
rgb_image.save(str(output_path), 'PNG')
elif image.mode == 'RGB':
image.save(str(output_path), 'PNG')
else:
image.convert('RGB').save(str(output_path), 'PNG')
image_saved = True
if image_saved:
full_path = output_path.resolve()
print(f"\nImage saved: {full_path}")
else:
print("Error: No image was generated in the response.", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Error generating image: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
+7
View File
@@ -0,0 +1,7 @@
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "portainer",
"installedVersion": "1.0.0",
"installedAt": 1771773615728
}
+308
View File
@@ -0,0 +1,308 @@
---
name: portainer
description: Control Docker containers and stacks via Portainer API. List containers, start/stop/restart, view logs, and redeploy stacks from git.
metadata: {"clawdbot":{"emoji":"🐳","requires":{"bins":["curl","jq"],"env":["PORTAINER_API_KEY"]},"primaryEnv":"PORTAINER_API_KEY"}}
---
# 🐳 Portainer Skill
```
╔═══════════════════════════════════════════════════════════╗
║ ║
║ 🐳 P O R T A I N E R C O N T R O L C L I 🐳 ║
║ ║
║ Manage Docker containers via Portainer API ║
║ Start, stop, deploy, redeploy ║
║ ║
╚═══════════════════════════════════════════════════════════╝
```
> *"Docker containers? I'll handle them from my lily pad."* 🐸
---
## 📖 What Does This Skill Do?
The **Portainer Skill** gives you control over your Docker infrastructure through Portainer's REST API. Manage containers, stacks, and deployments without touching the web UI.
**Features:**
- 📊 **Status** — Check Portainer server status
- 🖥️ **Endpoints** — List all Docker environments
- 📦 **Containers** — List, start, stop, restart containers
- 📚 **Stacks** — List and manage Docker Compose stacks
- 🔄 **Redeploy** — Pull from git and redeploy stacks
- 📜 **Logs** — View container logs
---
## ⚙️ Requirements
| What | Details |
|------|---------|
| **Portainer** | Version 2.x with API access |
| **Tools** | `curl`, `jq` |
| **Auth** | API Access Token |
### Setup
1. **Get API Token from Portainer:**
- Log into Portainer web UI
- Click username → My Account
- Scroll to "Access tokens" → Add access token
- Copy the token (you won't see it again!)
2. **Configure credentials:**
```bash
# Add to ~/.clawdbot/.env
PORTAINER_URL=https://your-portainer-server:9443
PORTAINER_API_KEY=ptr_your_token_here
```
3. **Ready!** 🚀
---
## 🛠️ Commands
### `status` — Check Portainer Server
```bash
./portainer.sh status
```
**Output:**
```
Portainer v2.27.3
```
---
### `endpoints` — List Environments
```bash
./portainer.sh endpoints
```
**Output:**
```
3: portainer (local) - ✓ online
4: production (remote) - ✓ online
```
---
### `containers` — List Containers
```bash
# List containers on default endpoint (4)
./portainer.sh containers
# List containers on specific endpoint
./portainer.sh containers 3
```
**Output:**
```
steinbergerraum-web-1 running Up 2 days
cora-web-1 running Up 6 weeks
minecraft running Up 6 weeks (healthy)
```
---
### `stacks` — List All Stacks
```bash
./portainer.sh stacks
```
**Output:**
```
25: steinbergerraum - ✓ active
33: cora - ✓ active
35: minecraft - ✓ active
4: pulse-website - ✗ inactive
```
---
### `stack-info` — Stack Details
```bash
./portainer.sh stack-info 25
```
**Output:**
```json
{
"Id": 25,
"Name": "steinbergerraum",
"Status": 1,
"EndpointId": 4,
"GitConfig": "https://github.com/user/repo",
"UpdateDate": "2026-01-25T08:44:56Z"
}
```
---
### `redeploy` — Pull & Redeploy Stack 🔄
```bash
./portainer.sh redeploy 25
```
**Output:**
```
✓ Stack 'steinbergerraum' redeployed successfully
```
This will:
1. Pull latest code from git
2. Rebuild containers if needed
3. Restart the stack
---
### `start` / `stop` / `restart` — Container Control
```bash
# Start a container
./portainer.sh start steinbergerraum-web-1
# Stop a container
./portainer.sh stop steinbergerraum-web-1
# Restart a container
./portainer.sh restart steinbergerraum-web-1
# Specify endpoint (default: 4)
./portainer.sh restart steinbergerraum-web-1 4
```
**Output:**
```
✓ Container 'steinbergerraum-web-1' restarted
```
---
### `logs` — View Container Logs
```bash
# Last 100 lines (default)
./portainer.sh logs steinbergerraum-web-1
# Last 50 lines
./portainer.sh logs steinbergerraum-web-1 4 50
```
---
## 🎯 Example Workflows
### 🚀 "Deploy Website Update"
```bash
# After merging PR
./portainer.sh redeploy 25
./portainer.sh logs steinbergerraum-web-1 4 20
```
### 🔧 "Debug Container"
```bash
./portainer.sh containers
./portainer.sh logs cora-web-1
./portainer.sh restart cora-web-1
```
### 📊 "System Overview"
```bash
./portainer.sh status
./portainer.sh endpoints
./portainer.sh containers
./portainer.sh stacks
```
---
## 🔧 Troubleshooting
### ❌ "Authentication required / Repository not found"
**Problem:** Stack redeploy fails with git auth error
**Solution:** The stack needs `repositoryGitCredentialID` parameter. The script handles this automatically by reading from the existing stack config.
---
### ❌ "Container not found"
**Problem:** Container name doesn't match
**Solution:** Use exact name from `./portainer.sh containers`:
- Include the full name: `steinbergerraum-web-1` not `steinbergerraum`
- Names are case-sensitive
---
### ❌ "PORTAINER_URL and PORTAINER_API_KEY must be set"
**Problem:** Credentials not configured
**Solution:**
```bash
# Add to ~/.clawdbot/.env
echo "PORTAINER_URL=https://your-server:9443" >> ~/.clawdbot/.env
echo "PORTAINER_API_KEY=ptr_your_token" >> ~/.clawdbot/.env
```
---
## 🔗 Integration with Clawd
```
"Redeploy the website"
→ ./portainer.sh redeploy 25
"Show me running containers"
→ ./portainer.sh containers
"Restart the Minecraft server"
→ ./portainer.sh restart minecraft
"What stacks do we have?"
→ ./portainer.sh stacks
```
---
## 📜 Changelog
| Version | Date | Changes |
|---------|------|---------|
| 1.0.0 | 2026-01-25 | Initial release |
---
## 🐸 Credits
```
@..@
(----)
( >__< ) "Containers are just fancy lily pads
^^ ^^ for your code to hop around!"
```
**Author:** Andy Steinberger (with help from his Clawdbot Owen the Frog 🐸)
**Powered by:** [Portainer](https://portainer.io/) API
**Part of:** [Clawdbot](https://clawdhub.com) Skills Collection
---
<div align="center">
**Made with 💚 for the Clawdbot Community**
*Ribbit!* 🐸
</div>
+6
View File
@@ -0,0 +1,6 @@
{
"ownerId": "kn708cyzn4v97wzx09d6zz7j3s7zx1t2",
"slug": "portainer",
"version": "1.0.0",
"publishedAt": 1769330831658
}
+185
View File
@@ -0,0 +1,185 @@
#!/bin/bash
# Portainer CLI - Control Docker containers via Portainer API
# Author: Andy Steinberger (with help from his Clawdbot Owen the Frog 🐸)
set -e
# Load config from environment or .env
PORTAINER_URL="${PORTAINER_URL:-}"
PORTAINER_API_KEY="${PORTAINER_API_KEY:-}"
# Try to load from clawdbot .env if not set
if [[ -z "$PORTAINER_URL" || -z "$PORTAINER_API_KEY" ]]; then
ENV_FILE="$HOME/.clawdbot/.env"
if [[ -f "$ENV_FILE" ]]; then
export $(grep -E "^PORTAINER_" "$ENV_FILE" | xargs)
fi
fi
if [[ -z "$PORTAINER_URL" || -z "$PORTAINER_API_KEY" ]]; then
echo "Error: PORTAINER_URL and PORTAINER_API_KEY must be set"
echo "Add to ~/.clawdbot/.env or export as environment variables"
exit 1
fi
API="$PORTAINER_URL/api"
AUTH_HEADER="X-API-Key: $PORTAINER_API_KEY"
# Helper function for API calls
api_get() {
curl -s -H "$AUTH_HEADER" "$API$1"
}
api_post() {
curl -s -X POST -H "$AUTH_HEADER" -H "Content-Type: application/json" "$API$1" -d "$2"
}
api_put() {
curl -s -X PUT -H "$AUTH_HEADER" -H "Content-Type: application/json" "$API$1" -d "$2"
}
# Commands
case "$1" in
status)
api_get "/status" | jq -r '"Portainer v\(.Version)"'
;;
endpoints|envs)
api_get "/endpoints" | jq -r '.[] | "\(.Id): \(.Name) (\(.Type == 1 | if . then "local" else "remote" end)) - \(if .Status == 1 then "✓ online" else "✗ offline" end)"'
;;
containers)
ENDPOINT="${2:-4}"
api_get "/endpoints/$ENDPOINT/docker/containers/json?all=true" | jq -r '.[] | "\(.Names[0] | ltrimstr("/"))\t\(.State)\t\(.Status)"' | column -t -s $'\t'
;;
stacks)
api_get "/stacks" | jq -r '.[] | "\(.Id): \(.Name) - \(if .Status == 1 then "✓ active" else "✗ inactive" end)"'
;;
stack-info)
STACK_ID="$2"
if [[ -z "$STACK_ID" ]]; then
echo "Usage: portainer.sh stack-info <stack-id>"
exit 1
fi
api_get "/stacks/$STACK_ID" | jq '{Id, Name, Status, EndpointId, GitConfig: .GitConfig.URL, UpdateDate: (.UpdateDate | todate)}'
;;
redeploy)
STACK_ID="$2"
if [[ -z "$STACK_ID" ]]; then
echo "Usage: portainer.sh redeploy <stack-id> [endpoint-id]"
exit 1
fi
# Get stack info for env vars and endpoint
STACK_INFO=$(api_get "/stacks/$STACK_ID")
ENDPOINT_ID=$(echo "$STACK_INFO" | jq -r '.EndpointId')
ENV_VARS=$(echo "$STACK_INFO" | jq -c '.Env')
GIT_CRED_ID=$(echo "$STACK_INFO" | jq -r '.GitConfig.Authentication.GitCredentialID // 0')
PAYLOAD=$(jq -n \
--argjson env "$ENV_VARS" \
--argjson gitCredId "$GIT_CRED_ID" \
'{env: $env, prune: false, pullImage: true, repositoryAuthentication: true, repositoryGitCredentialID: $gitCredId}')
RESULT=$(api_put "/stacks/$STACK_ID/git/redeploy?endpointId=$ENDPOINT_ID" "$PAYLOAD")
if echo "$RESULT" | jq -e '.Id' > /dev/null 2>&1; then
STACK_NAME=$(echo "$RESULT" | jq -r '.Name')
echo "✓ Stack '$STACK_NAME' redeployed successfully"
else
echo "✗ Redeploy failed:"
echo "$RESULT" | jq -r '.message // .details // .'
exit 1
fi
;;
start)
ENDPOINT="${3:-4}"
CONTAINER="$2"
if [[ -z "$CONTAINER" ]]; then
echo "Usage: portainer.sh start <container-name> [endpoint-id]"
exit 1
fi
# Get container ID
CONTAINER_ID=$(api_get "/endpoints/$ENDPOINT/docker/containers/json?all=true" | jq -r ".[] | select(.Names[0] == \"/$CONTAINER\") | .Id")
if [[ -z "$CONTAINER_ID" ]]; then
echo "✗ Container '$CONTAINER' not found"
exit 1
fi
api_post "/endpoints/$ENDPOINT/docker/containers/$CONTAINER_ID/start" "{}" > /dev/null
echo "✓ Container '$CONTAINER' started"
;;
stop)
ENDPOINT="${3:-4}"
CONTAINER="$2"
if [[ -z "$CONTAINER" ]]; then
echo "Usage: portainer.sh stop <container-name> [endpoint-id]"
exit 1
fi
CONTAINER_ID=$(api_get "/endpoints/$ENDPOINT/docker/containers/json?all=true" | jq -r ".[] | select(.Names[0] == \"/$CONTAINER\") | .Id")
if [[ -z "$CONTAINER_ID" ]]; then
echo "✗ Container '$CONTAINER' not found"
exit 1
fi
api_post "/endpoints/$ENDPOINT/docker/containers/$CONTAINER_ID/stop" "{}" > /dev/null
echo "✓ Container '$CONTAINER' stopped"
;;
restart)
ENDPOINT="${3:-4}"
CONTAINER="$2"
if [[ -z "$CONTAINER" ]]; then
echo "Usage: portainer.sh restart <container-name> [endpoint-id]"
exit 1
fi
CONTAINER_ID=$(api_get "/endpoints/$ENDPOINT/docker/containers/json?all=true" | jq -r ".[] | select(.Names[0] == \"/$CONTAINER\") | .Id")
if [[ -z "$CONTAINER_ID" ]]; then
echo "✗ Container '$CONTAINER' not found"
exit 1
fi
api_post "/endpoints/$ENDPOINT/docker/containers/$CONTAINER_ID/restart" "{}" > /dev/null
echo "✓ Container '$CONTAINER' restarted"
;;
logs)
ENDPOINT="${3:-4}"
CONTAINER="$2"
TAIL="${4:-100}"
if [[ -z "$CONTAINER" ]]; then
echo "Usage: portainer.sh logs <container-name> [endpoint-id] [tail-lines]"
exit 1
fi
CONTAINER_ID=$(api_get "/endpoints/$ENDPOINT/docker/containers/json?all=true" | jq -r ".[] | select(.Names[0] == \"/$CONTAINER\") | .Id")
if [[ -z "$CONTAINER_ID" ]]; then
echo "✗ Container '$CONTAINER' not found"
exit 1
fi
curl -s -H "$AUTH_HEADER" "$API/endpoints/$ENDPOINT/docker/containers/$CONTAINER_ID/logs?stdout=true&stderr=true&tail=$TAIL" | strings
;;
*)
echo "Portainer CLI - Control Docker via Portainer API"
echo ""
echo "Usage: portainer.sh <command> [args]"
echo ""
echo "Commands:"
echo " status Show Portainer version"
echo " endpoints List all environments"
echo " containers [endpoint] List containers (default endpoint: 4)"
echo " stacks List all stacks"
echo " stack-info <id> Show stack details"
echo " redeploy <stack-id> Pull and redeploy a stack"
echo " start <container> Start a container"
echo " stop <container> Stop a container"
echo " restart <container> Restart a container"
echo " logs <container> [ep] [n] Show container logs (last n lines)"
echo ""
echo "Environment:"
echo " PORTAINER_URL Portainer server URL"
echo " PORTAINER_API_KEY API access token"
;;
esac
+7
View File
@@ -0,0 +1,7 @@
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "proxmox-full",
"installedVersion": "1.0.0",
"installedAt": 1771710212894
}
+313
View File
@@ -0,0 +1,313 @@
---
name: proxmox-full
description: Complete Proxmox VE management - create/clone/start/stop VMs and LXC containers, manage snapshots, backups, storage, and templates. Use when user wants to manage Proxmox infrastructure, virtual machines, or containers.
metadata: {"clawdbot":{"emoji":"🖥️","homepage":"https://www.proxmox.com/","requires":{"bins":["curl","jq"],"env":["PVE_TOKEN"]},"primaryEnv":"PVE_TOKEN"}}
---
# Proxmox VE - Full Management
Complete control over Proxmox VE hypervisor via REST API.
## Setup
```bash
export PVE_URL="https://192.168.1.10:8006"
export PVE_TOKEN="user@pam!tokenid=secret-uuid"
```
**Create API token:** Datacenter → Permissions → API Tokens → Add (uncheck Privilege Separation)
## Auth Header
```bash
AUTH="Authorization: PVEAPIToken=$PVE_TOKEN"
```
---
## Cluster & Nodes
```bash
# Cluster status
curl -sk -H "$AUTH" "$PVE_URL/api2/json/cluster/status" | jq
# List nodes
curl -sk -H "$AUTH" "$PVE_URL/api2/json/nodes" | jq '.data[] | {node, status, cpu: (.cpu*100|round), mem_pct: (.mem/.maxmem*100|round)}'
# Node details
curl -sk -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/status" | jq
```
---
## List VMs & Containers
```bash
# All VMs on node
curl -sk -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/qemu" | jq '.data[] | {vmid, name, status}'
# All LXC on node
curl -sk -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/lxc" | jq '.data[] | {vmid, name, status}'
# Cluster-wide (all VMs + LXC)
curl -sk -H "$AUTH" "$PVE_URL/api2/json/cluster/resources?type=vm" | jq '.data[] | {node, type, vmid, name, status}'
```
---
## VM/Container Control
```bash
# Start
curl -sk -X POST -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/qemu/{vmid}/status/start"
# Stop (immediate)
curl -sk -X POST -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/qemu/{vmid}/status/stop"
# Shutdown (graceful)
curl -sk -X POST -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/qemu/{vmid}/status/shutdown"
# Reboot
curl -sk -X POST -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/qemu/{vmid}/status/reboot"
# For LXC: replace /qemu/ with /lxc/
```
---
## Create LXC Container
```bash
# Get next available VMID
NEWID=$(curl -sk -H "$AUTH" "$PVE_URL/api2/json/cluster/nextid" | jq -r '.data')
# Create container
curl -sk -X POST -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/lxc" \
-d "vmid=$NEWID" \
-d "hostname=my-container" \
-d "ostemplate=local:vztmpl/debian-12-standard_12.2-1_amd64.tar.zst" \
-d "storage=local-lvm" \
-d "rootfs=local-lvm:8" \
-d "memory=1024" \
-d "swap=512" \
-d "cores=2" \
-d "net0=name=eth0,bridge=vmbr0,ip=dhcp" \
-d "password=changeme123" \
-d "start=1"
```
**LXC Parameters:**
| Param | Example | Description |
|-------|---------|-------------|
| vmid | 200 | Container ID |
| hostname | myct | Container hostname |
| ostemplate | local:vztmpl/debian-12-... | Template path |
| storage | local-lvm | Storage for rootfs |
| rootfs | local-lvm:8 | Root disk (8GB) |
| memory | 1024 | RAM in MB |
| swap | 512 | Swap in MB |
| cores | 2 | CPU cores |
| net0 | name=eth0,bridge=vmbr0,ip=dhcp | Network config |
| password | secret | Root password |
| ssh-public-keys | ssh-rsa ... | SSH keys (URL encoded) |
| unprivileged | 1 | Unprivileged container |
| start | 1 | Start after creation |
---
## Create VM
```bash
# Get next VMID
NEWID=$(curl -sk -H "$AUTH" "$PVE_URL/api2/json/cluster/nextid" | jq -r '.data')
# Create VM
curl -sk -X POST -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/qemu" \
-d "vmid=$NEWID" \
-d "name=my-vm" \
-d "memory=2048" \
-d "cores=2" \
-d "sockets=1" \
-d "cpu=host" \
-d "net0=virtio,bridge=vmbr0" \
-d "scsi0=local-lvm:32" \
-d "scsihw=virtio-scsi-pci" \
-d "ide2=local:iso/ubuntu-22.04.iso,media=cdrom" \
-d "boot=order=scsi0;ide2;net0" \
-d "ostype=l26"
```
**VM Parameters:**
| Param | Example | Description |
|-------|---------|-------------|
| vmid | 100 | VM ID |
| name | myvm | VM name |
| memory | 2048 | RAM in MB |
| cores | 2 | CPU cores per socket |
| sockets | 1 | CPU sockets |
| cpu | host | CPU type |
| net0 | virtio,bridge=vmbr0 | Network |
| scsi0 | local-lvm:32 | Disk (32GB) |
| ide2 | local:iso/file.iso,media=cdrom | ISO |
| ostype | l26 (Linux), win11 | OS type |
| boot | order=scsi0;ide2 | Boot order |
---
## Clone VM/Container
```bash
# Clone VM
curl -sk -X POST -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/qemu/{vmid}/clone" \
-d "newid=201" \
-d "name=cloned-vm" \
-d "full=1" \
-d "storage=local-lvm"
# Clone LXC
curl -sk -X POST -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/lxc/{vmid}/clone" \
-d "newid=202" \
-d "hostname=cloned-ct" \
-d "full=1" \
-d "storage=local-lvm"
```
**Clone Parameters:**
| Param | Description |
|-------|-------------|
| newid | New VMID |
| name/hostname | New name |
| full | 1=full clone, 0=linked clone |
| storage | Target storage |
| target | Target node (for migration) |
---
## Convert to Template
```bash
# Convert VM to template
curl -sk -X POST -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/qemu/{vmid}/template"
# Convert LXC to template
curl -sk -X POST -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/lxc/{vmid}/template"
```
---
## Snapshots
```bash
# List snapshots
curl -sk -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/qemu/{vmid}/snapshot" | jq '.data[] | {name, description}'
# Create snapshot
curl -sk -X POST -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/qemu/{vmid}/snapshot" \
-d "snapname=before-update" \
-d "description=Snapshot before system update"
# Rollback
curl -sk -X POST -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/qemu/{vmid}/snapshot/{snapname}/rollback"
# Delete snapshot
curl -sk -X DELETE -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/qemu/{vmid}/snapshot/{snapname}"
```
---
## Backups
```bash
# Start backup
curl -sk -X POST -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/vzdump" \
-d "vmid={vmid}" \
-d "storage=local" \
-d "mode=snapshot" \
-d "compress=zstd"
# List backups
curl -sk -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/storage/{storage}/content?content=backup" | jq
# Restore backup
curl -sk -X POST -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/qemu" \
-d "vmid=300" \
-d "archive=local:backup/vzdump-qemu-100-2024_01_01-12_00_00.vma.zst" \
-d "storage=local-lvm"
```
---
## Storage & Templates
```bash
# List storage
curl -sk -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/storage" | jq '.data[] | {storage, type, avail, used}'
# List available templates
curl -sk -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/storage/local/content?content=vztmpl" | jq '.data[] | .volid'
# List ISOs
curl -sk -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/storage/local/content?content=iso" | jq '.data[] | .volid'
# Download template
curl -sk -X POST -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/aplinfo" \
-d "storage=local" \
-d "template=debian-12-standard_12.2-1_amd64.tar.zst"
```
---
## Tasks
```bash
# Recent tasks
curl -sk -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/tasks?limit=10" | jq '.data[] | {upid, type, status}'
# Task status
curl -sk -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/tasks/{upid}/status" | jq
# Task log
curl -sk -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/tasks/{upid}/log" | jq -r '.data[].t'
```
---
## Delete VM/Container
```bash
# Delete VM (must be stopped)
curl -sk -X DELETE -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/qemu/{vmid}"
# Delete LXC
curl -sk -X DELETE -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/lxc/{vmid}"
# Force delete (purge)
curl -sk -X DELETE -H "$AUTH" "$PVE_URL/api2/json/nodes/{node}/qemu/{vmid}?purge=1&destroy-unreferenced-disks=1"
```
---
## Quick Reference
| Action | Endpoint | Method |
|--------|----------|--------|
| List nodes | /nodes | GET |
| List VMs | /nodes/{node}/qemu | GET |
| List LXC | /nodes/{node}/lxc | GET |
| Create VM | /nodes/{node}/qemu | POST |
| Create LXC | /nodes/{node}/lxc | POST |
| Clone | /nodes/{node}/qemu/{vmid}/clone | POST |
| Start | /nodes/{node}/qemu/{vmid}/status/start | POST |
| Stop | /nodes/{node}/qemu/{vmid}/status/stop | POST |
| Snapshot | /nodes/{node}/qemu/{vmid}/snapshot | POST |
| Delete | /nodes/{node}/qemu/{vmid} | DELETE |
| Next ID | /cluster/nextid | GET |
## Notes
- Use `-k` for self-signed certs
- API tokens don't need CSRF
- Replace `{node}` with node name (e.g., `pve`)
- Replace `{vmid}` with VM/container ID
- Use `qemu` for VMs, `lxc` for containers
- All create/clone operations return task UPID for tracking
+6
View File
@@ -0,0 +1,6 @@
{
"ownerId": "kn7bt5ncxngtb52n7b20sb1x2x7zb567",
"slug": "proxmox-full",
"version": "1.0.0",
"publishedAt": 1769279195141
}