Skill: chloepark85/velog-cli (version 0.1.0)

--- name: Velog CLI slug: velog-cli version: 0.1.0 summary: Fetch latest posts for any Velog user via public RSS (no API key). labels: ["latest", "korea", "blog", "rss"] --- Velog public RSS CLI — 최신 Velog 포스트를 손쉽게 가져오는 도구이다. 인증이나 키가 필요하지 않으며 `https://v2.velog.io/rss/<username>` 피드를 파싱하여 Markdown 또는 JSON으로 출력한다. ## 사용법 ``` velog-cli user-posts --username <name> --limit 5 --format md ``` ## 설치 ``` pipx install . # 또는 uv tool install . ``` ## 참고 - Velog: https://velog.io - RSS: https://v2.velog.io/rss/<username>

path: CHANGELOG.md
content: # Changelog

## v0.1.0 — 2026-04-21
- Initial release: `velog-cli user-posts` (RSS-based, no auth)


path: README.md
content: # velog-cli

Velog public RSS/GraphQL CLI — fetch latest posts for any Velog user without API keys.

- No auth required (uses public RSS: `https://v2.velog.io/rss/<username>`)
- Output as Markdown (default) or JSON
- Filter by count

## Quickstart

```bash
pipx install .  # or: uv tool install .
velog-cli user-posts --username velopert --limit 5 --format md
```

Example output (md):

```
- [Understanding React Hooks](https://velog.io/@velopert/react-hooks) — 2026-04-01
- [TypeScript Utility Types](https://velog.io/@velopert/ts-utility) — 2026-03-27
```

## CLI

```
velog-cli user-posts --username <name> [--limit 10] [--format md|json]
```

## Why

ClawHub currently lacks a Velog integration while Korean developer communities rely heavily on Velog. This tool fills a common need: quickly pulling a user's latest posts for newsletters, digests, or agent context.

## Configuration

No environment variables are required.

## References
- Official site: https://velog.io
- Public RSS: https://v2.velog.io/rss/<username>
```


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

[project]
name = "velog-cli"
version = "0.1.0"
description = "Velog public RSS/GraphQL CLI — fetch latest posts for any Velog user without API keys."
readme = "README.md"
authors = [{ name = "Chloe" }]
license = { text = "MIT" }
requires-python = ">=3.8"
dependencies = ["requests>=2.28.0"]

[project.scripts]
velog-cli = "velog_cli.cli:main"


path: src/velog_cli/__init__.py
content: __all__ = ["__version__"]
__version__ = "0.1.0"


path: src/velog_cli/cli.py
content: import argparse
import json
from .core import fetch_user_rss, parse_rss


def cmd_user_posts(args: argparse.Namespace) -> int:
    xml_text = fetch_user_rss(args.username)
    items = parse_rss(xml_text, limit=args.limit)
    if args.format == "json":
        print(json.dumps(items, ensure_ascii=False, indent=2))
    else:
        for it in items:
            date = f" — {it['date']}" if it.get('date') else ""
            print(f"- [{it['title']}]({it['link']}){date}")
    return 0


def build_parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(prog="velog-cli", description="Velog public RSS CLI")
    sub = p.add_subparsers(dest="cmd", required=True)

    p_user = sub.add_parser("user-posts", help="Fetch latest posts for a user")
    p_user.add_argument("--username", required=True, help="Velog username (without @)")
    p_user.add_argument("--limit", type=int, default=10, help="Number of posts to fetch")
    p_user.add_argument("--format", choices=["md", "json"], default="md")
    p_user.set_defaults(func=cmd_user_posts)

    return p


def main() -> int:
    parser = build_parser()
    args = parser.parse_args()
    return args.func(args)


if __name__ == "__main__":
    raise SystemExit(main())


path: src/velog_cli/core.py
content: import datetime as dt
import xml.etree.ElementTree as ET
from typing import List, Dict
import requests

RSS_URL = "https://v2.velog.io/rss/{username}"


def fetch_user_rss(username: str) -> str:
    url = RSS_URL.format(username=username)
    resp = requests.get(url, timeout=15)
    resp.raise_for_status()
    return resp.text


def parse_rss(xml_text: str, limit: int = 10) -> List[Dict]:
    root = ET.fromstring(xml_text)
    # Typical RSS: <rss><channel><item>...</item></channel></rss>
    channel = root.find("channel")
    if channel is None:
        # Sometimes Atom? Try alternate namespace-aware
        # Fallback: return empty
        return []
    items = channel.findall("item")
    results = []
    for item in items[: max(1, limit)]:
        title = (item.findtext("title") or "").strip()
        link = (item.findtext("link") or "").strip()
        pub_date = (item.findtext("pubDate") or "").strip()
        # Normalize date if possible
        iso_date = pub_date
        try:
            # Example: Tue, 09 Apr 2024 13:24:00 GMT
            dt_obj = dt.datetime.strptime(pub_date, "%a, %d %b %Y %H:%M:%S %Z")
            iso_date = dt_obj.date().isoformat()
        except Exception:
            pass
        results.append({
            "title": title,
            "link": link,
            "date": iso_date,
        })
    return results


path: tests/test_smoke.sh
content: #!/usr/bin/env bash
set -euo pipefail
python -c "import velog_cli, sys; print(velog_cli.__version__)"
python -m velog_cli.cli -h >/dev/null 2>&1 || true


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