Skill: dzwalker/poe-connector (version 1.1.0)

--- name: poe-connector version: 1.1.0 author: dzwalker description: > Access Poe.com AI models by running bash commands. Use when user mentions Poe, wants to use Poe models, or generate media via Poe. IMPORTANT: This is a bash-tool skill. You MUST use the bash/shell tool to execute commands. Do NOT use sessions_spawn, do NOT use browser, do NOT try to use Poe as a model provider. triggers: - poe - Poe - poe.com - ask poe - use poe - via poe - poe chat - poe image - poe video - poe audio - poe models repository: https://github.com/dzwalker/poe-connector license: MIT dependencies: [] tools: - poe_chat - poe_media - poe_models metadata: {"clawdbot":{"emoji":"🤖","requires":{"anyBins":["python3","bash"],"config":["skills.entries.poe-connector.env.POE_API_KEY"]},"os":["linux","darwin","win32"]}} --- # Poe Connector — USE BASH TOOL **You MUST use the `bash` or `shell` tool to run these commands. No other method works.** ## Commands Chat with AI: ```bash bash ~/.openclaw/workspace/skills/poe-connector/poe.sh chat Tell me 10 jokes ``` Generate image: ```bash bash ~/.openclaw/workspace/skills/poe-connector/poe.sh image A sunset over mountains ``` Generate video: ```bash bash ~/.openclaw/workspace/skills/poe-connector/poe.sh video A cat playing piano ``` Generate audio: ```bash bash ~/.openclaw/workspace/skills/poe-connector/poe.sh audio Hello world in a cheerful voice ``` List models: ```bash bash ~/.openclaw/workspace/skills/poe-connector/poe.sh models ``` Search models: ```bash bash ~/.openclaw/workspace/skills/poe-connector/poe.sh search claude ``` ## Sending generated media (images/video/audio) to the user After generating an image/video/audio, the script prints a `[MEDIA_SEND_HINT]` line with the local file path. **You MUST use the `message` tool to deliver the file** — do NOT just paste the URL or embed it in markdown. Telegram cannot render markdown images (`![alt](url)`); it requires `sendPhoto` / `sendDocument` which the `message` tool handles automatically. ``` message send --media "/path/to/poe_generated_image.png" ``` If `message send` is not available, you can also use the `--to last` flag or specify the channel explicitly. Always include a short text caption so the user knows what the media is about. ## DO NOT - Do NOT embed image/media URLs in markdown like `![image](url)` — Telegram will NOT display them - Do NOT use `sessions_spawn` — it will fail - Do NOT use browser to visit poe.com — it will fail - Do NOT set poe as a model provider — it will fail - Do NOT use any approach other than `bash` tool — only bash works ## Setup (required before first use) This skill requires a Poe API key. Get one at https://poe.com/api/keys, then add to `~/.openclaw/openclaw.json`: ```json { "skills": { "entries": { "poe-connector": { "env": { "POE_API_KEY": "your-key" } } } } } ``` ## Notes - All commands use sensible default models (configured in poe_config.json) - If you get a POE_API_KEY error, tell the user to run the setup above - File attachments (--files) are base64-encoded and sent to Poe — do not attach sensitive files unless the user intends to share them with Poe

path: README.md
content: # Poe Connector — OpenClaw Skill

An [OpenClaw](https://github.com/openclaw/openclaw) skill that connects your agent to [Poe.com](https://poe.com)'s API, giving it access to **100+ AI models** through a single API key.

## Why This Exists

I've been a Poe user since it first launched — there's always a new model to try and I genuinely enjoy the platform. Last year I accidentally subscribed to a pretty expensive plan, and I've never been able to use up all the credits. So I figured: why not hook it up to OpenClaw and see if that burns through them a bit faster?


## Features

- **Text Chat** — Talk to Claude, GPT, Gemini, Grok, Llama, and hundreds more via `/v1/chat/completions`
- **Image Generation** — GPT-Image-1, Imagen-4, Flux-Kontext, Seedream-3.0
- **Video Generation** — Veo-3, Runway-Gen-4-Turbo, Kling-2.1
- **Audio / TTS** — ElevenLabs, Lyria
- **Multimodal File Input** — Send images, PDFs, audio, video alongside your prompt
- **Model Discovery** — List and search all available Poe models
- **Streaming** — Real-time token-by-token output
- **Auto Retry** — Exponential backoff on rate-limit (429) errors

## Quick Start

### 1. Get your Poe API key

Go to https://poe.com/api/keys and create an API key.

### 2. Configure the API key

Add your key to `~/.openclaw/openclaw.json`:

```json
{
  "skills": {
    "entries": {
      "poe-connector": {
        "env": { "POE_API_KEY": "your-key-here" }
      }
    }
  }
}
```

The skill reads the key from this file at runtime. No environment variable needed.

### 3. Install dependency

```bash
pip install openai
```

### 4. Use it

```bash
# Chat (uses default model from poe_config.json, streams by default)
python src/poe_chat.py --message "Hello!"

# Chat with a specific model
python src/poe_chat.py --model GPT-5.2 --message "Write a haiku"

# Generate an image (default: GPT-Image-1)
python src/poe_media.py --type image --prompt "A cat astronaut" --aspect 1:1

# List models
python src/poe_models.py --list

# Search models
python src/poe_models.py --search "claude"
```

### 5. Customize defaults

Edit `poe_config.json` to set your preferred models:

```json
{
  "defaults": {
    "chat": "claude-sonnet-4",
    "image": "GPT-Image-1",
    "video": "Veo-3",
    "audio": "ElevenLabs"
  }
}
```

## Project Structure

```
poe-connector/
├── SKILL.md              # OpenClaw skill definition
├── poe.sh                # Wrapper script (simplifies agent calls)
├── claw.json             # ClawHub publishing manifest
├── instructions.md       # Agent usage guide
├── requirements.txt      # Python dependencies
├── src/
│   ├── poe_utils.py      # Auth, error handling, file encoding
│   ├── poe_chat.py       # Text chat (streaming, multi-turn, file input)
│   ├── poe_media.py      # Image / video / audio generation
│   └── poe_models.py     # Model listing and search
└── examples/
    └── basic_usage.md    # Usage examples
```

## CLI Reference

### poe_chat.py

| Flag | Description |
|---|---|
| `--model, -m` | Model name (optional, defaults from poe_config.json) |
| `--message, -M` | User message |
| `--messages` | Full conversation as JSON array |
| `--system, -s` | System prompt |
| `--stream` | Stream output token by token |
| `--files, -f` | Attach files (images, PDFs, audio, video) |
| `--max-tokens` | Max response tokens |
| `--temperature` | Sampling temperature (0–2) |
| `--extra` | Custom params as JSON (`{"thinking_budget": 10000}`) |

### poe_media.py

| Flag | Description |
|---|---|
| `--type, -t` | `image`, `video`, or `audio` (required) |
| `--model, -m` | Model name (defaults per type) |
| `--prompt, -p` | Generation prompt (required) |
| `--output, -o` | Output file path |
| `--aspect` | Aspect ratio (e.g. `1:1`, `3:2`, `16:9`) |
| `--quality` | Quality level (`low`, `medium`, `high`) |
| `--extra` | Additional params as JSON |

### poe_models.py

| Flag | Description |
|---|---|
| `--list, -l` | List all available models |
| `--search, -s` | Search by keyword |
| `--info, -i` | Detailed info for a model |
| `--json` | Output as JSON |

## OpenClaw Installation

```bash
clawhub install dzwalker/poe-connector
```

Or manually copy to `~/.openclaw/workspace/skills/poe-connector/`.

## Requirements

- Python 3.9+
- `openai` >= 1.0.0
- Poe account with API key


## License

MIT


path: claw.json
content: {
  "name": "poe-connector",
  "version": "1.1.0",
  "description": "Connect OpenClaw to Poe.com — access 100+ AI models (Claude, GPT, Gemini, Grok, Llama), generate images/video/audio, and list available models, all through a single API key.",
  "author": "dzwalker",
  "category": "ai-ml",
  "tags": [
    "poe",
    "ai",
    "chatbot",
    "llm",
    "image-generation",
    "video-generation",
    "audio-generation",
    "multimodal",
    "openai-compatible"
  ],
  "permissions": [
    "network"
  ],
  "entry": "SKILL.md",
  "support": "https://github.com/dzwalker/poe-connector/issues",
  "license": "MIT",
  "engines": {
    "python": ">=3.9"
  },
  "dependencies": {
    "openai": ">=1.0.0"
  }
}


path: examples/basic_usage.md
content: # Poe Connector — Usage Examples

## Setup

```bash
# Install dependency
pip install openai

# Set your API key (get one at https://poe.com/api/keys)
export POE_API_KEY="your-key-here"
```

---

## Text Chat

### Simple question

```bash
python3 src/poe_chat.py --model Claude-Sonnet-4 --message "What is the capital of France?"
```

### With a system prompt

```bash
python3 src/poe_chat.py --model GPT-5.2 \
  --system "You are a helpful cooking assistant who gives concise recipes." \
  --message "How do I make pasta carbonara?"
```

### Streaming output

```bash
python3 src/poe_chat.py --model Gemini-3-Pro \
  --message "Write a short story about a robot learning to paint" \
  --stream
```

### Multi-turn conversation

```bash
python3 src/poe_chat.py --model Claude-Sonnet-4 \
  --messages '[
    {"role": "user", "content": "My name is Alice."},
    {"role": "assistant", "content": "Nice to meet you, Alice!"},
    {"role": "user", "content": "What is my name?"}
  ]'
```

### With custom parameters (extended thinking)

```bash
python3 src/poe_chat.py --model Claude-Sonnet-4 \
  --message "Solve this complex math problem step by step: ..." \
  --extra '{"thinking_budget": 20000}'
```

### With temperature control

```bash
python3 src/poe_chat.py --model GPT-5.2 \
  --message "Write a creative poem about the moon" \
  --temperature 1.5
```

---

## File Analysis

### Describe an image

```bash
python3 src/poe_chat.py --model Claude-Sonnet-4 \
  --message "What do you see in this image?" \
  --files photo.jpg
```

### Summarize a PDF

```bash
python3 src/poe_chat.py --model GPT-5.2 \
  --message "Summarize the key points of this document" \
  --files report.pdf
```

### Compare multiple images

```bash
python3 src/poe_chat.py --model Claude-Sonnet-4 \
  --message "Compare these two images and describe the differences" \
  --files before.png after.png
```

---

## Image Generation

### Basic image

```bash
python3 src/poe_media.py --type image \
  --prompt "A serene Japanese garden in autumn with a red bridge"
```

### With specific model and aspect ratio

```bash
python3 src/poe_media.py --type image \
  --model Imagen-4 \
  --prompt "Cyberpunk cityscape at night, neon lights" \
  --aspect 16:9 --quality high
```

### Save to specific path

```bash
python3 src/poe_media.py --type image \
  --model GPT-Image-1 \
  --prompt "A minimalist logo for a coffee shop called 'Bean There'" \
  --output logo.png
```

---

## Video Generation

```bash
python3 src/poe_media.py --type video \
  --model Veo-3 \
  --prompt "A timelapse of a flower blooming in a sunlit meadow"
```

---

## Audio / TTS

```bash
python3 src/poe_media.py --type audio \
  --model ElevenLabs \
  --prompt "Welcome to Poe Connector. This is a demonstration of text-to-speech capabilities."
```

---

## Model Discovery

### List all models

```bash
python3 src/poe_models.py --list
```

### Search for Claude models

```bash
python3 src/poe_models.py --search "claude"
```

### Search for image generation models

```bash
python3 src/poe_models.py --search "image"
```

### Get info on a specific model

```bash
python3 src/poe_models.py --info "Claude-Sonnet-4"
```

### Output as JSON (for programmatic use)

```bash
python3 src/poe_models.py --list --json
```


path: instructions.md
content: # Poe Connector — Agent Quick Reference

Run these with the **bash** tool. Nothing else works.

| Task | Command |
|---|---|
| Chat | `bash ~/.openclaw/workspace/skills/poe-connector/poe.sh chat <message>` |
| Image | `bash ~/.openclaw/workspace/skills/poe-connector/poe.sh image <prompt>` |
| Video | `bash ~/.openclaw/workspace/skills/poe-connector/poe.sh video <prompt>` |
| Audio | `bash ~/.openclaw/workspace/skills/poe-connector/poe.sh audio <text>` |
| List models | `bash ~/.openclaw/workspace/skills/poe-connector/poe.sh models` |
| Search models | `bash ~/.openclaw/workspace/skills/poe-connector/poe.sh search <keyword>` |

## ⚠ Sending generated media to the user

After image/video/audio generation, the script auto-downloads the file and prints a `[MEDIA_SEND_HINT]`.
**You MUST use the `message` tool to send the file** — do NOT embed URLs in markdown text.

```
message send --media "/path/to/poe_generated_image.png"
```

Telegram cannot render markdown images (`![alt](url)`) — only `sendPhoto` works, which the `message` tool handles.


path: poe.sh
content: #!/bin/bash
# Poe Connector - wrapper script for OpenClaw agent
# Usage: bash ~/.openclaw/workspace/skills/poe-connector/poe <command> [args]
SKILL_DIR="$(cd "$(dirname "$0")" && pwd)"
VENV_ACTIVATE="$SKILL_DIR/.venv/bin/activate"
if [ -f "$VENV_ACTIVATE" ] && [ ! -L "$VENV_ACTIVATE" ]; then
  source "$VENV_ACTIVATE"
fi

case "$1" in
  chat)
    shift
    python3 "$SKILL_DIR/src/poe_chat.py" --message "$*"
    ;;
  ask)
    shift
    python3 "$SKILL_DIR/src/poe_chat.py" --message "$*"
    ;;
  image)
    shift
    python3 "$SKILL_DIR/src/poe_media.py" --type image --prompt "$*"
    ;;
  video)
    shift
    python3 "$SKILL_DIR/src/poe_media.py" --type video --prompt "$*"
    ;;
  audio)
    shift
    python3 "$SKILL_DIR/src/poe_media.py" --type audio --prompt "$*"
    ;;
  models)
    shift
    python3 "$SKILL_DIR/src/poe_models.py" --list "$@"
    ;;
  search)
    shift
    python3 "$SKILL_DIR/src/poe_models.py" --search "$*"
    ;;
  *)
    echo "Poe Connector commands:"
    echo "  bash $0 chat <message>     Chat with default model"
    echo "  bash $0 image <prompt>     Generate an image"
    echo "  bash $0 video <prompt>     Generate a video"
    echo "  bash $0 audio <text>       Text-to-speech"
    echo "  bash $0 models             List all models"
    echo "  bash $0 search <keyword>   Search models"
    ;;
esac


path: poe_config.json
content: {
  "defaults": {
    "chat": "claude-sonnet-4",
    "image": "GPT-Image-1",
    "video": "Veo-3",
    "audio": "ElevenLabs"
  },
  "models": {
    "chat": [
      "claude-sonnet-4",
      "claude-opus-4.5",
      "claude-haiku-4.5",
      "GPT-5.2",
      "Gemini-3-Pro",
      "Grok-4",
      "Llama-3.1-405B",
      "deepseek-r1"
    ],
    "image": [
      "GPT-Image-1",
      "Imagen-4",
      "Flux-Kontext",
      "Seedream-3.0",
      "dall-e-3"
    ],
    "video": [
      "Veo-3",
      "Runway-Gen-4-Turbo",
      "Kling-2.1"
    ],
    "audio": [
      "ElevenLabs",
      "Lyria"
    ]
  },
  "chat_options": {
    "stream": true,
    "max_tokens": null,
    "temperature": null
  }
}


path: requirements.txt
content: openai>=1.0.0


path: src/poe_chat.py
content: #!/usr/bin/env python3
"""Poe Connector — Text chat completions (streaming and non-streaming) with file input support."""

import argparse
import json
import sys
import os

sys.path.insert(0, os.path.dirname(__file__))

from poe_utils import (
    get_client,
    retry_on_rate_limit,
    handle_api_error,
    parse_extra_body,
    build_file_messages,
    get_default_model,
    get_chat_options,
)
import openai


def build_messages(
    message: str | None,
    messages_json: str | None,
    system: str | None,
    files: list[str] | None,
) -> list[dict]:
    """Assemble the messages list from CLI arguments."""
    msgs: list[dict] = []

    if system:
        msgs.append({"role": "system", "content": system})

    if messages_json:
        try:
            cleaned = messages_json.lstrip("\ufeff").strip()
            parsed = json.loads(cleaned)
        except json.JSONDecodeError as e:
            print(f"Error: Invalid JSON for --messages: {e}", file=sys.stderr)
            sys.exit(1)
        if not isinstance(parsed, list):
            print("Error: --messages must be a JSON array.", file=sys.stderr)
            sys.exit(1)
        msgs.extend(parsed)
    elif message:
        if files:
            msgs.extend(build_file_messages(message, files))
        else:
            msgs.append({"role": "user", "content": message})
    else:
        print("Error: provide either --message or --messages.", file=sys.stderr)
        sys.exit(1)

    return msgs


def chat(
    model: str,
    messages: list[dict],
    stream: bool = False,
    extra_body: dict | None = None,
    max_tokens: int | None = None,
    temperature: float | None = None,
) -> None:
    """Send a chat completion request to Poe and print the response."""
    client = get_client()

    kwargs: dict = {"model": model, "messages": messages}
    if max_tokens is not None:
        kwargs["max_tokens"] = max_tokens
    if temperature is not None:
        kwargs["temperature"] = temperature
    if extra_body:
        kwargs["extra_body"] = extra_body

    if stream:
        kwargs["stream"] = True

        def do_stream():
            return client.chat.completions.create(**kwargs)

        response = retry_on_rate_limit(do_stream)
        try:
            for chunk in response:
                delta = chunk.choices[0].delta.content
                if delta:
                    print(delta, end="", flush=True)
            print()
        except openai.APIError as e:
            handle_api_error(e)
            sys.exit(1)
    else:
        def do_chat():
            return client.chat.completions.create(**kwargs)

        try:
            response = retry_on_rate_limit(do_chat)
        except openai.APIError as e:
            handle_api_error(e)
            sys.exit(1)

        content = response.choices[0].message.content
        print(content)

        usage = response.usage
        if usage:
            print(
                f"\n--- Usage: {usage.prompt_tokens} prompt + "
                f"{usage.completion_tokens} completion = "
                f"{usage.total_tokens} total tokens ---",
                file=sys.stderr,
            )


def main():
    parser = argparse.ArgumentParser(
        description="Chat with AI models via the Poe API.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=(
            "Examples:\n"
            '  %(prog)s --model Claude-Sonnet-4 --message "Hello"\n'
            '  %(prog)s --model GPT-5.2 --system "You are a pirate." --message "Ahoy" --stream\n'
            "  %(prog)s --model Claude-Sonnet-4 --message \"Describe this\" --files photo.jpg\n"
        ),
    )
    parser.add_argument("--model", "-m", help="Poe model name (default from poe_config.json)")
    parser.add_argument("--message", "-M", help="User message text")
    parser.add_argument("--messages", help="Full conversation as a JSON array of {role, content} objects")
    parser.add_argument("--system", "-s", help="System prompt")
    parser.add_argument("--stream", action="store_true", default=None, help="Stream the response token by token")
    parser.add_argument("--no-stream", dest="stream", action="store_false", help="Disable streaming")
    parser.add_argument("--files", "-f", nargs="+", help="File paths to attach (images, PDFs, audio, video)")
    parser.add_argument("--max-tokens", type=int, help="Maximum tokens in the response")
    parser.add_argument("--temperature", type=float, help="Sampling temperature (0-2)")
    parser.add_argument("--extra", help='Extra body params as JSON (e.g. \'{"thinking_budget": 10000}\')')

    args = parser.parse_args()

    model = args.model or get_default_model("chat")
    if not model:
        print("Error: no --model given and no default in poe_config.json", file=sys.stderr)
        sys.exit(1)

    opts = get_chat_options()
    stream = args.stream if args.stream is not None else opts.get("stream", False)
    max_tokens = args.max_tokens if args.max_tokens is not None else opts.get("max_tokens")
    temperature = args.temperature if args.temperature is not None else opts.get("temperature")

    messages = build_messages(args.message, args.messages, args.system, args.files)
    extra = parse_extra_body(args.extra)

    chat(
        model=model,
        messages=messages,
        stream=stream,
        extra_body=extra,
        max_tokens=max_tokens,
        temperature=temperature,
    )


if __name__ == "__main__":
    main()


path: src/poe_media.py
content: #!/usr/bin/env python3
"""Poe Connector — Image, video, and audio generation via Poe media models."""
from __future__ import annotations

import argparse
import base64
import re
import sys
import os
import urllib.request
from pathlib import Path

sys.path.insert(0, os.path.dirname(__file__))

from poe_utils import get_client, retry_on_rate_limit, handle_api_error, parse_extra_body, get_default_model
import openai

FALLBACK_MODELS = {
    "image": "GPT-Image-1",
    "video": "Veo-3",
    "audio": "ElevenLabs",
}

DEFAULT_EXTENSIONS = {
    "image": ".png",
    "video": ".mp4",
    "audio": ".mp3",
}


def _auto_download(url: str, output_path: str, media_type: str) -> bool:
    """Download *url* to *output_path*. Returns True on success."""
    try:
        req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 poe-connector/1.0"})
        with urllib.request.urlopen(req, timeout=30) as resp:
            Path(output_path).write_bytes(resp.read())
        return True
    except Exception as e:
        print(f"Warning: auto-download failed — {e}", file=sys.stderr)
        return False


def extract_and_save_media(content: str, output_path: str | None, media_type: str) -> str | None:
    """Try to find a base64 data URL or a regular URL in the response content and save/report it.

    When no explicit *output_path* is given, the file is auto-downloaded to
    the working directory so that the caller (typically an OpenClaw agent) has
    a local path to hand off to the ``message`` tool for Telegram delivery.
    """
    data_url_match = re.search(r"data:([^;]+);base64,([A-Za-z0-9+/=]+)", content)
    if data_url_match:
        mime, b64_data = data_url_match.group(1), data_url_match.group(2)
        raw = base64.b64decode(b64_data)
        if not output_path:
            ext = DEFAULT_EXTENSIONS.get(media_type, ".bin")
            output_path = f"poe_generated_{media_type}{ext}"
        Path(output_path).write_bytes(raw)
        print(f"Saved {media_type} to: {output_path}")
        _print_media_hint(output_path, None, media_type)
        return output_path

    url_match = re.search(r"https?://\S+", content)
    if url_match:
        url = url_match.group(0).rstrip(")")
        if not output_path:
            ext = DEFAULT_EXTENSIONS.get(media_type, ".bin")
            output_path = str(Path.cwd() / f"poe_generated_{media_type}{ext}")

        if _auto_download(url, output_path, media_type):
            local_path = str(Path(output_path).resolve())
            print(f"Saved {media_type} to: {local_path}")
            print(f"Source URL: {url}")
            _print_media_hint(local_path, url, media_type)
            return local_path
        else:
            print(f"Generated {media_type} URL: {url}")
            _print_media_hint(None, url, media_type)
            return url

    return None


def _print_media_hint(local_path: str | None, url: str | None, media_type: str) -> None:
    """Print a structured hint so the agent knows how to deliver media on Telegram."""
    target = local_path or url
    if not target:
        return
    print(
        f"\n[MEDIA_SEND_HINT] Use the message tool to send this {media_type} to the user:\n"
        f'  message send --media "{target}"'
    )


def generate_media(
    model: str,
    prompt: str,
    media_type: str,
    output: str | None = None,
    extra_body: dict | None = None,
) -> None:
    """Send a media generation request to Poe and handle the response."""
    client = get_client()

    kwargs: dict = {
        "model": model,
        "messages": [{"role": "user", "content": prompt}],
        "stream": False,
    }
    if extra_body:
        kwargs["extra_body"] = extra_body

    def do_generate():
        return client.chat.completions.create(**kwargs)

    try:
        response = retry_on_rate_limit(do_generate)
    except openai.APIError as e:
        handle_api_error(e)
        sys.exit(1)

    content = response.choices[0].message.content
    if not content:
        print("Error: Empty response from model.", file=sys.stderr)
        sys.exit(1)

    saved = extract_and_save_media(content, output, media_type)
    if not saved:
        print(content)

    usage = response.usage
    if usage:
        print(
            f"\n--- Usage: {usage.prompt_tokens} prompt + "
            f"{usage.completion_tokens} completion = "
            f"{usage.total_tokens} total tokens ---",
            file=sys.stderr,
        )


def main():
    parser = argparse.ArgumentParser(
        description="Generate images, videos, and audio via Poe media models.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=(
            "Examples:\n"
            '  %(prog)s --type image --prompt "A sunset over mountains"\n'
            '  %(prog)s --type image --model Imagen-4 --prompt "Cyberpunk city" --aspect 16:9\n'
            '  %(prog)s --type video --model Veo-3 --prompt "A cat playing piano"\n'
            '  %(prog)s --type audio --model ElevenLabs --prompt "Hello world"\n'
        ),
    )
    parser.add_argument(
        "--type", "-t",
        required=True,
        choices=["image", "video", "audio"],
        help="Media type to generate",
    )
    parser.add_argument("--model", "-m", help="Poe model name (defaults vary by type)")
    parser.add_argument("--prompt", "-p", required=True, help="Generation prompt / text")
    parser.add_argument("--output", "-o", help="Output file path (auto-named if omitted)")
    parser.add_argument("--aspect", help='Aspect ratio for image models (e.g. "1:1", "3:2", "16:9")')
    parser.add_argument("--quality", help='Quality level for image models (e.g. "low", "medium", "high")')
    parser.add_argument("--extra", help='Additional params as JSON (e.g. \'{"duration": 5}\')')

    args = parser.parse_args()
    model = args.model or get_default_model(args.type) or FALLBACK_MODELS.get(args.type, "GPT-Image-1")

    extra = parse_extra_body(args.extra) or {}
    if args.aspect:
        extra["aspect"] = args.aspect
    if args.quality:
        extra["quality"] = args.quality

    generate_media(
        model=model,
        prompt=args.prompt,
        media_type=args.type,
        output=args.output,
        extra_body=extra or None,
    )


if __name__ == "__main__":
    main()


path: src/poe_models.py
content: #!/usr/bin/env python3
"""Poe Connector — List, search, and inspect available Poe models."""

import argparse
import json
import sys
import os

sys.path.insert(0, os.path.dirname(__file__))

from poe_utils import get_client, handle_api_error
import openai


def fetch_models() -> list[dict]:
    """Fetch all available models from the Poe API."""
    client = get_client()
    try:
        response = client.models.list()
    except openai.APIError as e:
        handle_api_error(e)
        sys.exit(1)

    models = []
    for m in response.data:
        entry = {"id": m.id, "created": m.created, "owned_by": m.owned_by}
        if hasattr(m, "object"):
            entry["object"] = m.object
        models.append(entry)
    return models


def list_models(as_json: bool = False) -> None:
    """Print all available models."""
    models = fetch_models()
    if as_json:
        print(json.dumps(models, indent=2))
        return

    print(f"Available Poe models ({len(models)} total):\n")
    for m in sorted(models, key=lambda x: x["id"].lower()):
        owner = m.get("owned_by", "")
        owner_str = f"  (by {owner})" if owner else ""
        print(f"  {m['id']}{owner_str}")
    print(f"\nTotal: {len(models)} models")


def search_models(query: str, as_json: bool = False) -> None:
    """Search models by keyword (case-insensitive)."""
    models = fetch_models()
    query_lower = query.lower()
    matches = [
        m for m in models
        if query_lower in m["id"].lower() or query_lower in (m.get("owned_by") or "").lower()
    ]

    if as_json:
        print(json.dumps(matches, indent=2))
        return

    if not matches:
        print(f'No models matching "{query}".')
        return

    print(f'Models matching "{query}" ({len(matches)} found):\n')
    for m in sorted(matches, key=lambda x: x["id"].lower()):
        owner = m.get("owned_by", "")
        owner_str = f"  (by {owner})" if owner else ""
        print(f"  {m['id']}{owner_str}")


def model_info(model_id: str) -> None:
    """Show detailed info for a specific model."""
    models = fetch_models()
    match = next((m for m in models if m["id"].lower() == model_id.lower()), None)

    if not match:
        close = [m for m in models if model_id.lower() in m["id"].lower()]
        print(f'Model "{model_id}" not found.', file=sys.stderr)
        if close:
            print(f"\nDid you mean one of these?", file=sys.stderr)
            for m in close[:10]:
                print(f"  {m['id']}", file=sys.stderr)
        sys.exit(1)

    print(json.dumps(match, indent=2))


def main():
    parser = argparse.ArgumentParser(
        description="List, search, and inspect Poe models.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=(
            "Examples:\n"
            "  %(prog)s --list\n"
            "  %(prog)s --list --json\n"
            '  %(prog)s --search "claude"\n'
            '  %(prog)s --info "Claude-Sonnet-4"\n'
        ),
    )
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument("--list", "-l", action="store_true", help="List all available models")
    group.add_argument("--search", "-s", metavar="QUERY", help="Search models by keyword")
    group.add_argument("--info", "-i", metavar="MODEL", help="Show detailed info for a model")
    parser.add_argument("--json", action="store_true", help="Output as JSON")

    args = parser.parse_args()

    if args.list:
        list_models(as_json=args.json)
    elif args.search:
        search_models(args.search, as_json=args.json)
    elif args.info:
        model_info(args.info)


if __name__ == "__main__":
    main()


path: src/poe_utils.py
content: """Shared utilities for Poe Connector: authentication, error handling, file encoding."""

import sys
import io
import base64
import mimetypes
import time
import json
from pathlib import Path

if sys.stdout.encoding and sys.stdout.encoding.lower() not in ("utf-8", "utf8"):
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")

try:
    import openai
except ImportError:
    print("Error: openai package not installed. Run: pip install openai", file=sys.stderr)
    sys.exit(1)

POE_BASE_URL = "https://api.poe.com/v1"

MIME_MAP = {
    ".jpg": "image/jpeg",
    ".jpeg": "image/jpeg",
    ".png": "image/png",
    ".gif": "image/gif",
    ".webp": "image/webp",
    ".svg": "image/svg+xml",
    ".pdf": "application/pdf",
    ".mp3": "audio/mp3",
    ".wav": "audio/wav",
    ".mp4": "video/mp4",
    ".webm": "video/webm",
}

MAX_RETRIES = 3
INITIAL_BACKOFF_S = 0.5

_config_cache: dict | None = None
SKILL_ROOT = Path(__file__).resolve().parent.parent


def load_config() -> dict:
    """Load poe_config.json from the skill root. Returns empty dict on failure."""
    global _config_cache
    if _config_cache is not None:
        return _config_cache

    config_path = SKILL_ROOT / "poe_config.json"
    if config_path.exists():
        try:
            _config_cache = json.loads(config_path.read_text(encoding="utf-8"))
        except (json.JSONDecodeError, OSError):
            _config_cache = {}
    else:
        _config_cache = {}
    return _config_cache


def get_default_model(task_type: str) -> str | None:
    """Get the default model for a task type (chat/image/video/audio)."""
    cfg = load_config()
    return cfg.get("defaults", {}).get(task_type)


def get_available_models(task_type: str) -> list[str]:
    """Get the configured model list for a task type."""
    cfg = load_config()
    return cfg.get("models", {}).get(task_type, [])


def get_chat_options() -> dict:
    """Get default chat options (stream, max_tokens, temperature)."""
    cfg = load_config()
    return cfg.get("chat_options", {})


def _load_key_from_openclaw_json() -> str | None:
    """Try to read POE_API_KEY from ~/.openclaw/openclaw.json."""
    config_path = Path.home() / ".openclaw" / "openclaw.json"
    if not config_path.exists():
        return None
    try:
        raw = config_path.read_text(encoding="utf-8").lstrip("\ufeff")
        cfg = json.loads(raw)
        return (
            cfg.get("skills", {})
            .get("entries", {})
            .get("poe-connector", {})
            .get("env", {})
            .get("POE_API_KEY")
        )
    except (json.JSONDecodeError, OSError):
        return None


def get_client() -> openai.OpenAI:
    """Create an OpenAI client configured for the Poe API."""
    [REDACTED_SECRET])
    if not [REDACTED_SECRET]
            "Error: POE_API_KEY is not configured.\n"
            "Get your key at https://poe.com/api/keys, then add it to:\n"
            "\n"
            "  ~/.openclaw/openclaw.json\n"
            '  { "skills": { "entries": { "poe-connector": { "env": { "POE_API_KEY": "your-key" } } } } }',
            file=sys.stderr,
        )
        sys.exit(1)
    return openai.OpenAI([REDACTED_SECRET], base_url=POE_BASE_URL)


def encode_file(file_path: str) -> dict:
    """Encode a local file as a base64 data-URL content block for the messages API.

    Returns an OpenAI-compatible content block (image_url or file type).
    """
    p = Path(file_path)
    if not p.exists():
        raise FileNotFoundError(f"File not found: {file_path}")

    suffix = p.suffix.lower()
    mime = MIME_MAP.get(suffix) or mimetypes.guess_type(file_path)[0] or "application/octet-stream"
    b64 = base64.b64encode(p.read_bytes()).decode("utf-8")
    data_url = f"data:{mime};base64,{b64}"

    if mime.startswith("image/"):
        return {"type": "image_url", "image_url": {"url": data_url}}
    else:
        return {"type": "file", "file": {"filename": p.name, "file_data": data_url}}


def build_file_messages(text: str, file_paths: list[str]) -> list[dict]:
    """Build a multimodal user message with text and attached files."""
    content: list[dict] = [{"type": "text", "text": text}]
    for fp in file_paths:
        content.append(encode_file(fp))
    return [{"role": "user", "content": content}]


def handle_api_error(e: openai.APIError) -> None:
    """Print a user-friendly error message for known Poe API errors."""
    code = getattr(e, "status_code", None)
    messages = {
        401: "Authentication failed — check your POE_API_KEY.",
        402: "Insufficient credits — purchase add-on points at https://poe.com/api/keys",
        403: "Permission denied — this model or action is not allowed.",
        404: "Model not found — run poe_models.py --list to see available models.",
        408: "Request timed out — the model took too long to respond. Try again.",
        413: "Request too large — reduce the input size (tokens exceed context window).",
        429: "Rate limit hit (500 rpm). Retrying with backoff...",
        500: "Provider error — the upstream model had an issue. Try again shortly.",
        502: "Upstream error — the model backend is temporarily unavailable.",
        529: "Service overloaded — transient traffic spike. Retry shortly.",
    }
    msg = messages.get(code, f"API error (HTTP {code}): {e}")
    print(f"Error: {msg}", file=sys.stderr)


def retry_on_rate_limit(func, *args, **kwargs):
    """Call *func* with automatic retry on 429 rate-limit errors."""
    backoff = INITIAL_BACKOFF_S
    for attempt in range(MAX_RETRIES + 1):
        try:
            return func(*args, **kwargs)
        except openai.RateLimitError:
            if attempt == MAX_RETRIES:
                print("Error: Rate limit exceeded after maximum retries.", file=sys.stderr)
                sys.exit(1)
            wait = backoff * (2 ** attempt)
            print(f"Rate limited. Retrying in {wait:.1f}s... (attempt {attempt + 1}/{MAX_RETRIES})", file=sys.stderr)
            time.sleep(wait)
        except openai.APIError as e:
            handle_api_error(e)
            sys.exit(1)


def parse_extra_body(raw: str | None) -> dict | None:
    """Parse a JSON string into a dict for extra_body, or return None."""
    if not raw:
        return None
    try:
        return json.loads(raw.lstrip("\ufeff").strip())
    except json.JSONDecodeError as e:
        print(f"Error: Invalid JSON for --extra: {e}", file=sys.stderr)
        sys.exit(1)


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)