Skill: chloepark85/imgur-cli (version 0.1.0)

--- name: imgur-cli description: Imgur API CLI for agents. Upload images by file path or URL, fetch image metadata, delete uploads via delete-hash, create and manage albums. Anonymous uploads with IMGUR_CLIENT_ID, or authenticated uploads with IMGUR_ACCESS_TOKEN. Useful as a media-hosting primitive for agents that post to Instagram, Discord, Reddit, Telegram, etc. Outputs JSON. Use when a user needs to host an image publicly, turn a local file into a shareable URL, archive URLs into albums, or embed images in downstream pipelines. license: MIT --- # Imgur CLI Wraps the official Imgur v3 API for AI agents. Upload images by file or URL, fetch metadata, delete by delete-hash, and manage albums — all as subcommands that print JSON. ## When to use Trigger this skill when: - An agent just produced an image (from `nano-banana-pro`, `generate-image`, ffmpeg, matplotlib, etc.) and another step needs a public URL. - A user asks to "host this image", "get a shareable link for this file", or "upload to Imgur". - You need to persist screenshots or render artifacts as shareable links. - Archiving URLs into Imgur albums for later retrieval. Pairs well with: `instagram-api` (needs hosted URLs), `nano-banana-pro`, `generate-image`, Discord/Telegram posting skills. ## Install ```bash pip install -e . ``` Then set either: - `IMGUR_CLIENT_ID` — anonymous uploads (get one at https://api.imgur.com/oauth2/addclient) - `IMGUR_ACCESS_TOKEN` — OAuth2 bearer token (takes precedence if both are set) ## Commands ```bash imgur-cli upload <file-or-url> [--title T] [--description D] [--album HASH] imgur-cli get <image-hash> imgur-cli delete <delete-hash-or-id> imgur-cli album-create [--title T] [--description D] [--privacy public|hidden|secret] [--image ID ...] imgur-cli album-add <album-hash> --image ID [--image ID ...] ``` All commands print the Imgur API `data` object as indented JSON. Errors exit non-zero with the Imgur error payload on stderr. ## Examples ```bash # Anonymous upload, grab the shareable link export IMGUR_CLIENT_ID="<client id>" imgur-cli upload ./photo.jpg --title "cat pic" | jq -r '.link' # Upload to user account export IMGUR_ACCESS_TOKEN="<oauth token>" imgur-cli upload https://example.com/pic.png --description "from pipeline" # Save the delete-hash for later cleanup imgur-cli upload ./tmp.png | jq -r '.deletehash' > .imgur-delete # Create a private album of existing images imgur-cli album-create --title "run-042" --privacy hidden --image aBc12 --image xYz34 ``` ## Notes - Max image size: 10MB; videos are not supported by this skill (images only). - Anonymous `deletehash` is the **only** way to remove anonymous uploads — store it. - Respect Imgur rate limits (see your developer dashboard).

path: CHANGELOG.md
content: # Changelog

## 0.1.0 — 2026-04-23
- Initial release.
- Commands: `upload`, `get`, `delete`, `album-create`, `album-add`.
- Auth via `IMGUR_CLIENT_ID` (anonymous) or `IMGUR_ACCESS_TOKEN` (OAuth).


path: README.md
content: # imgur-cli

Imgur API CLI for AI agents. Upload/get/delete images, manage albums. Works with anonymous uploads (Client-ID) or authenticated uploads (OAuth access token). Zero extra deps beyond `requests`.

- Official API: https://apidocs.imgur.com/
- Auth: `IMGUR_CLIENT_ID` (anonymous) **or** `IMGUR_ACCESS_TOKEN` (user-owned)

## Quickstart

```bash
pip install -e .
export IMGUR_CLIENT_ID="<your client id>"
imgur-cli upload ./photo.jpg --title "cat pic" | jq '.link'
```

## Commands

```bash
imgur-cli upload <file-or-url> [--title T] [--description D] [--album HASH]
imgur-cli get <image-hash>
imgur-cli delete <delete-hash-or-id>
imgur-cli album-create [--title T] [--description D] [--privacy public|hidden|secret] [--image ID ...]
imgur-cli album-add <album-hash> --image ID [--image ID ...]
```

All commands print the API response `data` object as indented JSON.

## Why this skill

AI agents that post to Instagram, Discord, Reddit, etc. frequently need a zero-friction image host. Imgur has a simple Client-ID upload flow and generous anonymous quotas — perfect as a hosting primitive for agent pipelines. No other ClawHub skill wraps Imgur directly.

## Environment variables
- `IMGUR_CLIENT_ID` — required for anonymous uploads (get one at https://api.imgur.com/oauth2/addclient)
- `IMGUR_ACCESS_TOKEN` — optional OAuth2 bearer token for uploads to a specific user account

If both are set, `IMGUR_ACCESS_TOKEN` takes precedence.

## Notes
- Anonymous uploads return a `deletehash` — save it if you want to delete later.
- Large files are base64-encoded; keep under Imgur's 10MB image limit.
- API rate limits apply (see Imgur dashboard).

## License
MIT


path: pyproject.toml
content: [build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "imgur-cli"
version = "0.1.0"
description = "Imgur API CLI — anonymous or OAuth uploads, image/album management, JSON output"
readme = "README.md"
authors = [{ name = "Chloe Park" }]
license = { text = "MIT" }
requires-python = ">=3.8"
dependencies = ["requests>=2.28.0"]

[project.scripts]
imgur-cli = "imgur_cli.cli:main"

[tool.setuptools.packages.find]
where = ["src"]


path: src/imgur_cli/__init__.py
content: __all__ = ["upload_image", "get_image", "delete_image", "create_album", "add_to_album"]
from .core import upload_image, get_image, delete_image, create_album, add_to_album


path: src/imgur_cli/cli.py
content: import argparse
import json
import sys
from typing import Any

from . import core


def _print(obj: Any) -> None:
    json.dump(obj, sys.stdout, ensure_ascii=False, indent=2)
    sys.stdout.write("\n")


def main() -> None:
    p = argparse.ArgumentParser(
        prog="imgur-cli",
        description="Imgur API CLI (anonymous Client-ID or OAuth access token)",
    )
    sub = p.add_subparsers(dest="cmd", required=True)

    up = sub.add_parser("upload", help="Upload an image (file path or URL)")
    up.add_argument("source", help="Local file path or http(s) URL")
    up.add_argument("--title")
    up.add_argument("--description")
    up.add_argument("--album", help="Album hash to attach the image to")

    g = sub.add_parser("get", help="Get image metadata by image hash/id")
    g.add_argument("image_hash")

    d = sub.add_parser("delete", help="Delete image by delete-hash (anonymous) or id (authenticated)")
    d.add_argument("delete_hash_or_id")

    ac = sub.add_parser("album-create", help="Create a new album")
    ac.add_argument("--title")
    ac.add_argument("--description")
    ac.add_argument("--privacy", choices=["public", "hidden", "secret"])
    ac.add_argument("--image", action="append", dest="image_ids", default=[], help="Image id; repeatable")

    aa = sub.add_parser("album-add", help="Add images to an existing album")
    aa.add_argument("album_hash")
    aa.add_argument("--image", action="append", dest="image_ids", default=[], required=True, help="Image id; repeatable")

    args = p.parse_args()

    try:
        if args.cmd == "upload":
            _print(core.upload_image(args.source, title=args.title, description=args.description, album=args.album))
        elif args.cmd == "get":
            _print(core.get_image(args.image_hash))
        elif args.cmd == "delete":
            _print(core.delete_image(args.delete_hash_or_id))
        elif args.cmd == "album-create":
            _print(core.create_album(title=args.title, description=args.description, privacy=args.privacy, image_ids=args.image_ids or None))
        elif args.cmd == "album-add":
            _print(core.add_to_album(args.album_hash, args.image_ids))
    except Exception as e:
        print(f"error: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()


path: src/imgur_cli/core.py
content: import base64
import os
from pathlib import Path
from typing import Any, Dict, List, Optional

import requests

BASE = "https://api.imgur.com/3"


def _auth_headers(access_token: Optional[str] = None, client_id: Optional[str] = None) -> Dict[str, str]:
    [REDACTED_SECRET] or os.getenv("IMGUR_ACCESS_TOKEN")
    if [REDACTED_SECRET] {"Authorization": f"Bearer {token}"}
    cid = client_id or os.getenv("IMGUR_CLIENT_ID")
    if not cid:
        raise RuntimeError(
            "Set IMGUR_CLIENT_ID (anonymous) or IMGUR_ACCESS_TOKEN (authenticated)."
        )
    return {"Authorization": f"Client-ID {cid}"}


def _unwrap(resp: requests.Response) -> Dict[str, Any]:
    try:
        body = resp.json()
    except ValueError:
        resp.raise_for_status()
        raise RuntimeError(f"Non-JSON response ({resp.status_code})")
    if not body.get("success", False):
        err = body.get("data", {}).get("error") or body.get("data") or body
        raise RuntimeError(f"Imgur API error ({resp.status_code}): {err}")
    return body.get("data", {})


def upload_image(
    source: str,
    *,
    title: Optional[str] = None,
    description: Optional[str] = None,
    album: Optional[str] = None,
    access_token: Optional[str] = None,
    client_id: Optional[str] = None,
    timeout: float = 60.0,
) -> Dict[str, Any]:
    headers = _auth_headers(access_token, client_id)
    data: Dict[str, Any] = {}
    if title:
        data["title"] = title
    if description:
        data["description"] = description
    if album:
        data["album"] = album

    files = None
    if source.startswith("http://") or source.startswith("https://"):
        data["type"] = "url"
        data["image"] = source
    else:
        path = Path(source).expanduser()
        if not path.is_file():
            raise FileNotFoundError(f"Not a file: {path}")
        data["type"] = "base64"
        data["image"] = base64.b64encode(path.read_bytes()).decode("ascii")

    resp = requests.post(f"{BASE}/image", headers=headers, data=data, files=files, timeout=timeout)
    return _unwrap(resp)


def get_image(image_hash: str, *, access_token: Optional[str] = None, client_id: Optional[str] = None, timeout: float = 30.0) -> Dict[str, Any]:
    headers = _auth_headers(access_token, client_id)
    resp = requests.get(f"{BASE}/image/{image_hash}", headers=headers, timeout=timeout)
    return _unwrap(resp)


def delete_image(delete_hash_or_id: str, *, access_token: Optional[str] = None, client_id: Optional[str] = None, timeout: float = 30.0) -> Dict[str, Any]:
    headers = _auth_headers(access_token, client_id)
    resp = requests.delete(f"{BASE}/image/{delete_hash_or_id}", headers=headers, timeout=timeout)
    return _unwrap(resp)


def create_album(
    *,
    title: Optional[str] = None,
    description: Optional[str] = None,
    privacy: Optional[str] = None,
    image_ids: Optional[List[str]] = None,
    access_token: Optional[str] = None,
    client_id: Optional[str] = None,
    timeout: float = 30.0,
) -> Dict[str, Any]:
    headers = _auth_headers(access_token, client_id)
    data: Dict[str, Any] = {}
    if title:
        data["title"] = title
    if description:
        data["description"] = description
    if privacy:
        data["privacy"] = privacy
    if image_ids:
        data["ids[]"] = image_ids
    resp = requests.post(f"{BASE}/album", headers=headers, data=data, timeout=timeout)
    return _unwrap(resp)


def add_to_album(
    album_hash: str,
    image_ids: List[str],
    *,
    access_token: Optional[str] = None,
    client_id: Optional[str] = None,
    timeout: float = 30.0,
) -> Dict[str, Any]:
    headers = _auth_headers(access_token, client_id)
    resp = requests.post(
        f"{BASE}/album/{album_hash}/add",
        headers=headers,
        data={"ids[]": image_ids},
        timeout=timeout,
    )
    return _unwrap(resp)


path: tests/test_smoke.sh
content: #!/usr/bin/env bash
set -euo pipefail
python -c 'import imgur_cli as x; print(all(hasattr(x, n) for n in ["upload_image","get_image","delete_image","create_album","add_to_album"]))'
python - <<'PY'
from imgur_cli.core import _auth_headers
import os
os.environ.pop("IMGUR_ACCESS_TOKEN", None)
os.environ["IMGUR_CLIENT_ID"] = "test"
h = _auth_headers()
assert h["Authorization"] == "Client-ID test", h
os.environ["IMGUR_ACCESS_TOKEN"] = "tok"
h = _auth_headers()
assert h["Authorization"] == "Bearer tok", h
print("auth header ok")
PY


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