feat: méthode WebSocket HA pour Lovelace + vue lumières créée
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 1,
|
||||
"registry": "https://clawhub.ai",
|
||||
"slug": "fal-ai",
|
||||
"installedVersion": "0.1.0",
|
||||
"installedAt": 1771426182485
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||

|
||||
|
||||
# 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)
|
||||
@@ -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.
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ownerId": "kn7bwx2qpadhr9wgbcqfr6q86h8098s2",
|
||||
"slug": "fal-ai",
|
||||
"version": "0.1.0",
|
||||
"publishedAt": 1769875535293
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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).
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ownerId": "kn739j7n05ptqcedg52zgnhrfh7zx24g",
|
||||
"slug": "home-assistant",
|
||||
"version": "1.0.0",
|
||||
"publishedAt": 1769638965135
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 you’re 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
|
||||
```
|
||||
@@ -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()
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 1,
|
||||
"registry": "https://clawhub.ai",
|
||||
"slug": "portainer",
|
||||
"installedVersion": "1.0.0",
|
||||
"installedAt": 1771773615728
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ownerId": "kn708cyzn4v97wzx09d6zz7j3s7zx1t2",
|
||||
"slug": "portainer",
|
||||
"version": "1.0.0",
|
||||
"publishedAt": 1769330831658
|
||||
}
|
||||
Executable
+185
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 1,
|
||||
"registry": "https://clawhub.ai",
|
||||
"slug": "proxmox-full",
|
||||
"installedVersion": "1.0.0",
|
||||
"installedAt": 1771710212894
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ownerId": "kn7bt5ncxngtb52n7b20sb1x2x7zb567",
|
||||
"slug": "proxmox-full",
|
||||
"version": "1.0.0",
|
||||
"publishedAt": 1769279195141
|
||||
}
|
||||
Reference in New Issue
Block a user