Claude Skill

lov-find-logo

Fetch a company/product logo from public sources (Clearbit, og:image, favicon) given a brand name or URL, score candidates (wide-aspect + transparent preferred), and archive the best + runner-ups to the configured logo collection directory. Trigger when the user says "find logo",

LLM Mart · 0 points · 9 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download lovstudio-skills-skills_find-logo-0b16007.zip · 11 KB
Part of lovstudio/skills — 83 skills

Install

skills CLI npx skills add https://github.com/lovstudio/skills/tree/main/skills/find-logo
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lovstudio-skills@llmmart
Git git clone https://github.com/lovstudio/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lovstudio/skills collection as a plugin from our marketplace. Git is the plain clone.

README

Logo 雷达 · Logo Radar

Version

Fetch a brand/product logo from public sources (Clearbit, og:image, favicon), score candidates (prefer wide-aspect + transparent), and archive the winner plus alternates under ~/.skill-publisher/logo-collection/<slug>/.

Useful for building website partner strips, PPT footer rows, poster credits — anywhere you need a lineup of logos that look consistent.

Part of skills — by example.com

Install

npx skills add find-logo -g -y

Stdlib only — no pip install needed.

Usage

# by name
python3 scripts/find_logo.py --name "Anthropic"

# by URL (more reliable for non-.com or ambiguous brands)
python3 scripts/find_logo.py --name "xAI" --url https://x.ai

# machine-readable output for chaining
python3 scripts/find_logo.py --url https://stripe.com --json

Options

Option Default Description
--name — Brand/product name. Used for slug + meta.
--url — Official URL or bare domain. Overrides the name-based guess.
--slug slugified name Override the archive directory slug.
--out SKILL_FIND_LOGO_OUTPUT_DIR or ~/.skill-publisher/logo-collection Archive root.
--keep-alts 2 How many runner-up candidates to keep.
--json off Emit JSON to stdout.

At least one of --name or --url is required.

What gets archived

~/.skill-publisher/logo-collection/<slug>/
├── logo.<ext>      # highest-scoring candidate
├── alt-1.<ext>     # runner-ups (count = --keep-alts)
├── alt-2.<ext>
└── meta.json       # brand, source URL per candidate, dims, format, alpha, score

How candidates are scored

Higher is better. Intended to bubble up wide-aspect + transparent logos so they line up cleanly next to each other.

Factor Points
Format SVG / PNG / WebP / JPG / ICO +40 / +20 / +10 / -10 / -20
Has alpha (transparent) +30
Aspect ≥ 2:1 (banner) +25
Aspect ≥ 1.3:1 (landscape) +10
Aspect ≈ 1:1 (square) -5
Aspect tall / portrait -15
Short edge ≥ 128 / ≥ 64 / < 32 px +15 / +5 / -20
Payload < 400 bytes (stub) -30

Sources probed, in order

  1. Clearbit Logo API — https://logo.clearbit.com/<domain> (unauthenticated tier).
  2. og:image / twitter:image + <link rel="icon"> — scraped from the brand's homepage. Usually wins because sites ship a proper share card.
  3. Google s2 favicon — https://www.google.com/s2/favicons?domain=<domain>&sz=256 as a safety net.

If all three fail, the script exits 2 with status: "no-candidates". The calling agent is expected to fall back to a web search for a press-kit page.

User Configuration

Archive files default to ~/.skill-publisher/logo-collection/. Override per call with --out, or set SKILL_FIND_LOGO_OUTPUT_DIR.

License

MIT

Skill manifest

Logo 雷达 · Logo Radar

Takes a brand name or URL, probes Clearbit + the site's own og:image / <link rel=icon> / favicon, scores each candidate, and archives the best one plus a couple of alternates into the configured collection directory.

When to Use

  • User asks to collect one or more brand logos for a slide/poster/site lineup
  • User names companies to drop into a partners/press strip
  • User gives a URL and wants its logo pulled down cleanly

Workflow (MANDATORY)

Step 1: Identify each brand

Accept any mix of names and URLs. If the user gave only a name with no obvious domain, ask — don't silently guess .com (script will guess, but for non-US or ambiguous brands that fails).

Use AskUserQuestion when:

  • Brand name is ambiguous (e.g. "Apple" = fruit vs. Inc.)
  • No URL and the domain isn't guessable (xAI → x.ai, not xai.com)
  • User gave a list without URLs

Step 2: Fetch — one brand per invocation

python3 scripts/find_logo.py --name "Anthropic" --url https://anthropic.com --json

For a batch, loop; the script is idempotent per <slug>/ (re-runs overwrite).

Step 3: Inspect score; fall back to WebSearch if needed

  • Exit code 0 → logo archived. The printed score is your quality signal:
    • ≥ 60 — solid: SVG or transparent PNG with wide/square aspect
    • 20–60 — usable: probably a favicon or small PNG
    • < 20 — weak: only ICO or tiny stub found
  • Exit code 2 / status: "no-candidates" → script found nothing. Do NOT give up. Use WebSearch for "<brand> logo svg site:*.com" or the brand's press-kit page, then re-invoke with --url <direct-image-url> is not supported — if you have a direct image URL, save it into the configured collection directory under <slug>/logo.<ext> and hand-write meta.json using the existing layout as a template.

Step 4: Report

Report back with the archive path and the primary's aspect + format. If the score is weak, tell the user and offer to retry with a specific press-kit URL or Wikipedia SVG.

CLI Reference

Argument Default Description
--name — Brand/product name. Used for slug + meta.
--url — Official URL or bare domain. Overrides the name-based domain guess.
--slug slugified name Override the directory slug under the archive root.
--out SKILL_FIND_LOGO_OUTPUT_DIR or ~/.skill-publisher/logo-collection Archive root.
--keep-alts 2 How many runner-up candidates to keep as alt-N.<ext>.
--json off Emit a JSON result to stdout (use this when chaining).

At least one of --name or --url is required.

Archive Layout

~/.skill-publisher/logo-collection/
├── anthropic/
│   ├── logo.png            # primary (highest score)
│   ├── alt-1.png           # runner-ups
│   ├── alt-2.png
│   └── meta.json           # sources, scores, dimensions, fetched_at
├── vercel/
│   ├── logo.png            # 1200x628 transparent banner
│   └── ...
└── stripe/
    ├── logo.svg
    └── ...

Scoring Heuristic (why a candidate wins)

  • Format: SVG (+40) > PNG (+20) > WebP (+10) > JPG (-10) > ICO (-20)
  • Transparency: +30 if alpha channel present (SVG always counts)
  • Aspect ratio: +25 for wide (≥2:1), +10 for landscape (≥1.3:1), -5 for square, -15 for tall/portrait
  • Short edge: +15 if ≥128px, +5 if ≥64px, -20 if <32px
  • Size sanity: -30 if payload <400 bytes (almost certainly a stub)

This matches the "prefer 长条形 + rgba" preference — wide transparent logos come out on top, square favicons land as alternates.

Dependencies

Stdlib only (urllib, html.parser, argparse). No pip install required.

User Configuration

Default archive files live under ~/.skill-publisher/logo-collection/. Override this per run with --out, or set SKILL_FIND_LOGO_OUTPUT_DIR for the skill.

Known Limits

  • The name → domain guess is a crude lowercase-strip + .com suffix. For anything not on .com, pass --url explicitly.
  • No Clearbit API key is used — we hit the unauthenticated endpoint, which covers most major brands but not all.
  • WebSearch fallback is Claude's responsibility, not the script's.

Runtime context (shared)

运行前读取本 Skill 包的 skill.yaml,由宿主提供 skill-runtime/v1 上下文。字段解析顺序为:当前请求、项目上下文、个人 Preferences、品牌 Profile、通用默认值。

  • 只使用 Manifest 声明的字段;Profile 保存公开品牌事实,Preferences 保存个人工作偏好。
  • required: true 字段缺失时,按 Manifest 的问题配置向用户提出一个聚焦问题;用户明确同意后再保存回答。
  • 报错提供可复制的 context_id、字段路径与来源,诊断内容避开秘密、完整私人路径和原始配置。

通用反馈闭环

用户在 Skill 驱动任务中提出修改意见时,继续当前产物前必须执行:

  1. 先判断意见是 task-specific(仅本次)还是 reusable(可跨任务复用)。
  2. task-specific 只修改当前任务,不改 Skill。
  3. reusable 先确定作用域:领域规则先更新对应 canonical Skill;适用于所有 Skill 的规则先更新共享规范。
  4. 完成规则更新、版本、lint 与分发核验后,再把修改应用到当前任务。
  5. reusable 修改会使此前的“确认”“继续”“发吧”失效;完成当前产物修改和回读后必须停下,等待用户下一步指示,不自动进入发布、提交或其他外部写入。
Files (skills)
  • scripts
    • find_logo.py 12.4 KB
      #!/usr/bin/env python3
      """find-logo — fetch a brand/product logo from public sources, score candidates,
      keep the best (prefer wide-aspect + transparent), archive to a per-brand dir.
      
      Sources tried (in order):
        1. Clearbit Logo API            https://logo.clearbit.com/<domain>
        2. Favicon (Google s2, 256px)   https://www.google.com/s2/favicons?domain=<domain>&sz=256
        3. og:image / twitter:image     scraped from https://<domain>/
        4. <link rel="icon"> variants   scraped from https://<domain>/
      
      The caller (Claude) is expected to run WebSearch as a fallback when this
      script exits with status 2 (no candidates). This script itself makes no
      search-engine calls — we keep the deterministic pipeline here.
      
      Usage:
        find_logo.py --name "Anthropic"
        find_logo.py --name "Anthropic" --url https://anthropic.com
        find_logo.py --url https://anthropic.com
        find_logo.py --name "OpenAI" --out ~/.skill-publisher/logo-collection
      """
      from __future__ import annotations
      
      import argparse
      import json
      import os
      import re
      import sys
      import time
      import urllib.parse
      import urllib.request
      from dataclasses import dataclass, asdict
      from html.parser import HTMLParser
      from pathlib import Path
      from typing import Optional
      
      UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15"
      DEFAULT_OUT = Path(
          os.environ.get("SKILL_FIND_LOGO_OUTPUT_DIR")
          or Path.home() / ".skill-publisher" / "logo-collection"
      )
      TIMEOUT = 10
      
      
      # ---------- helpers ----------
      
      def slugify(s: str) -> str:
          s = s.strip().lower()
          s = re.sub(r"[^a-z0-9\u4e00-\u9fff]+", "-", s)
          return s.strip("-") or "logo"
      
      
      def domain_from_url(url: str) -> str:
          p = urllib.parse.urlparse(url if "://" in url else f"https://{url}")
          return p.netloc or p.path
      
      
      def guess_domain(name: str) -> Optional[str]:
          """Very light guess: lowercase, strip spaces, try .com. Caller may override."""
          guess = re.sub(r"\s+", "", name.lower())
          guess = re.sub(r"[^a-z0-9-]", "", guess)
          return f"{guess}.com" if guess else None
      
      
      def http_get(url: str, *, binary: bool = False) -> tuple[int, bytes, dict]:
          req = urllib.request.Request(url, headers={"User-Agent": UA, "Accept": "*/*"})
          try:
              with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
                  data = r.read()
                  return r.status, data, dict(r.headers)
          except Exception as e:
              return 0, b"", {"error": str(e)}
      
      
      # ---------- image probing ----------
      
      def sniff_format(data: bytes) -> Optional[str]:
          if data.startswith(b"\x89PNG\r\n\x1a\n"):
              return "png"
          if data[:3] == b"GIF":
              return "gif"
          if data[:2] == b"\xff\xd8":
              return "jpg"
          if data[:4] == b"RIFF" and data[8:12] == b"WEBP":
              return "webp"
          if data[:6] in (b"<?xml ", b"<svg x") or b"<svg" in data[:256].lower():
              return "svg"
          if data[:4] == b"\x00\x00\x01\x00":
              return "ico"
          return None
      
      
      def png_size(data: bytes) -> Optional[tuple[int, int]]:
          if len(data) < 24 or data[:8] != b"\x89PNG\r\n\x1a\n":
              return None
          w = int.from_bytes(data[16:20], "big")
          h = int.from_bytes(data[20:24], "big")
          return w, h
      
      
      def png_has_alpha(data: bytes) -> bool:
          # color type byte at offset 25; 4 = GA, 6 = RGBA, and tRNS chunk → has alpha
          if len(data) < 26 or data[:8] != b"\x89PNG\r\n\x1a\n":
              return False
          ct = data[25]
          if ct in (4, 6):
              return True
          return b"tRNS" in data[:4096]
      
      
      def jpg_size(data: bytes) -> Optional[tuple[int, int]]:
          i = 2
          while i < len(data) - 9:
              if data[i] != 0xFF:
                  return None
              while data[i] == 0xFF:
                  i += 1
              marker = data[i]; i += 1
              if 0xC0 <= marker <= 0xCF and marker not in (0xC4, 0xC8, 0xCC):
                  return int.from_bytes(data[i+3:i+5], "big"), int.from_bytes(data[i+1:i+3], "big")
              seg_len = int.from_bytes(data[i:i+2], "big")
              i += seg_len
          return None
      
      
      def svg_size(data: bytes) -> Optional[tuple[int, int]]:
          head = data[:2048].decode("utf-8", errors="ignore")
          vb = re.search(r'viewBox=["\']\s*[\-0-9.]+\s+[\-0-9.]+\s+([0-9.]+)\s+([0-9.]+)', head)
          if vb:
              return int(float(vb.group(1))), int(float(vb.group(2)))
          w = re.search(r'\bwidth=["\']([0-9.]+)', head)
          h = re.search(r'\bheight=["\']([0-9.]+)', head)
          if w and h:
              return int(float(w.group(1))), int(float(h.group(1)))
          return None
      
      
      def probe(data: bytes) -> dict:
          fmt = sniff_format(data)
          size = None
          alpha = False
          if fmt == "png":
              size = png_size(data)
              alpha = png_has_alpha(data)
          elif fmt == "jpg":
              size = jpg_size(data)
          elif fmt == "svg":
              size = svg_size(data)
              alpha = True  # svg = vector, effectively transparent
          elif fmt == "webp":
              # cheap: assume alpha if VP8L or VP8X with alpha flag
              alpha = b"VP8L" in data[:64] or (b"VP8X" in data[:64] and (data[20] & 0x10) != 0)
          return {
              "format": fmt,
              "width": size[0] if size else None,
              "height": size[1] if size else None,
              "has_alpha": alpha,
              "bytes": len(data),
          }
      
      
      # ---------- og:image scraper ----------
      
      class MetaIconParser(HTMLParser):
          def __init__(self) -> None:
              super().__init__()
              self.og_image: Optional[str] = None
              self.tw_image: Optional[str] = None
              self.icons: list[tuple[str, Optional[str]]] = []  # (href, sizes)
      
          def handle_starttag(self, tag, attrs):
              a = dict(attrs)
              if tag == "meta":
                  prop = (a.get("property") or a.get("name") or "").lower()
                  if prop == "og:image" and a.get("content"):
                      self.og_image = a["content"]
                  elif prop == "twitter:image" and a.get("content"):
                      self.tw_image = a["content"]
              elif tag == "link":
                  rel = (a.get("rel") or "").lower()
                  if "icon" in rel and a.get("href"):
                      self.icons.append((a["href"], a.get("sizes")))
      
      
      def scrape_page_images(page_url: str) -> list[str]:
          status, body, _ = http_get(page_url)
          if status != 200 or not body:
              return []
          parser = MetaIconParser()
          try:
              parser.feed(body.decode("utf-8", errors="ignore"))
          except Exception:
              return []
          urls: list[str] = []
          for u in (parser.og_image, parser.tw_image):
              if u:
                  urls.append(urllib.parse.urljoin(page_url, u))
          # icons: prefer the largest declared sizes first
          def icon_rank(it):
              sz = it[1] or ""
              m = re.search(r"(\d+)x(\d+)", sz)
              return -(int(m.group(1)) if m else 0)
          for href, _ in sorted(parser.icons, key=icon_rank):
              urls.append(urllib.parse.urljoin(page_url, href))
          # de-dup, preserve order
          seen = set(); out = []
          for u in urls:
              if u not in seen:
                  seen.add(u); out.append(u)
          return out
      
      
      # ---------- scoring ----------
      
      def score(info: dict) -> float:
          """Higher is better. Rewards wide aspect, alpha, reasonable size, vector."""
          if not info.get("format"):
              return -1e9
          s = 0.0
          fmt = info["format"]
          if fmt == "svg":
              s += 40
          elif fmt == "png":
              s += 20
          elif fmt == "webp":
              s += 10
          elif fmt == "ico":
              s -= 20
          elif fmt == "jpg":
              s -= 10
      
          if info.get("has_alpha"):
              s += 30
      
          w, h = info.get("width"), info.get("height")
          if w and h and h > 0:
              ratio = w / h
              if ratio >= 2:
                  s += 25  # long / banner
              elif ratio >= 1.3:
                  s += 10
              elif 0.9 <= ratio <= 1.1:
                  s -= 5   # square — OK but not preferred
              else:
                  s -= 15  # tall
      
              short = min(w, h)
              if short >= 128:
                  s += 15
              elif short >= 64:
                  s += 5
              elif short < 32:
                  s -= 20
      
          b = info.get("bytes") or 0
          if b < 400:
              s -= 30  # probably a stub
          return s
      
      
      # ---------- pipeline ----------
      
      @dataclass
      class Candidate:
          source: str       # clearbit / favicon / og / icon-link / manual
          url: str
          data: bytes
          info: dict
          score: float
      
          def ext(self) -> str:
              return self.info.get("format") or "bin"
      
      
      def try_candidates(domain: str) -> list[Candidate]:
          cands: list[Candidate] = []
      
          def add(source: str, url: str) -> None:
              status, data, _ = http_get(url)
              if status != 200 or not data:
                  return
              info = probe(data)
              if not info["format"]:
                  return
              cands.append(Candidate(source, url, data, info, score(info)))
      
          # 1. Clearbit
          add("clearbit", f"https://logo.clearbit.com/{domain}")
      
          # 2. og:image + link[rel=icon] on https://<domain>/
          for src_url in scrape_page_images(f"https://{domain}/"):
              add("page", src_url)
              if len(cands) >= 8:
                  break
      
          # 3. Google s2 favicon (always resolves, ranked low)
          add("google-s2", f"https://www.google.com/s2/favicons?domain={domain}&sz=256")
      
          return cands
      
      
      def archive(best: Candidate, alts: list[Candidate], slug: str, brand_name: str,
                  source_domain: str, out_root: Path, keep_alts: int) -> Path:
          brand_dir = out_root / slug
          brand_dir.mkdir(parents=True, exist_ok=True)
          primary = brand_dir / f"logo.{best.ext()}"
          primary.write_bytes(best.data)
      
          meta = {
              "brand": brand_name,
              "slug": slug,
              "source_domain": source_domain,
              "fetched_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
              "primary": {
                  "file": primary.name,
                  "source": best.source,
                  "url": best.url,
                  "score": round(best.score, 2),
                  **best.info,
              },
              "alternates": [],
          }
      
          for i, c in enumerate(alts[:keep_alts]):
              alt_name = f"alt-{i+1}.{c.ext()}"
              (brand_dir / alt_name).write_bytes(c.data)
              meta["alternates"].append({
                  "file": alt_name,
                  "source": c.source,
                  "url": c.url,
                  "score": round(c.score, 2),
                  **c.info,
              })
      
          (brand_dir / "meta.json").write_text(json.dumps(meta, indent=2, ensure_ascii=False))
          return brand_dir
      
      
      def main() -> int:
          ap = argparse.ArgumentParser(description="Find & archive a brand logo.")
          ap.add_argument("--name", help="Brand / product name")
          ap.add_argument("--url", help="Official URL or domain (overrides name guess)")
          ap.add_argument("--slug", help="Override archive slug (default: slugified name/domain)")
          ap.add_argument("--out", default=str(DEFAULT_OUT), help=f"Archive root (default: {DEFAULT_OUT})")
          ap.add_argument("--keep-alts", type=int, default=2, help="How many runner-up candidates to keep")
          ap.add_argument("--json", action="store_true", help="Emit machine-readable JSON to stdout")
          args = ap.parse_args()
      
          if not args.name and not args.url:
              ap.error("need --name or --url")
      
          domain = domain_from_url(args.url) if args.url else guess_domain(args.name)
          if not domain:
              print("ERROR: could not derive domain; pass --url", file=sys.stderr)
              return 2
      
          brand_name = args.name or domain
          slug = args.slug or slugify(args.name or domain.split(".")[0])
          out_root = Path(os.path.expanduser(args.out))
      
          cands = try_candidates(domain)
          if not cands:
              msg = {"status": "no-candidates", "domain": domain, "brand": brand_name,
                     "hint": "fall back to WebSearch + manual --url"}
              print(json.dumps(msg) if args.json else f"no candidates for {domain} — try WebSearch")
              return 2
      
          cands.sort(key=lambda c: c.score, reverse=True)
          best, *rest = cands
          brand_dir = archive(best, rest, slug, brand_name, domain, out_root, args.keep_alts)
      
          result = {
              "status": "ok",
              "brand": brand_name,
              "slug": slug,
              "dir": str(brand_dir),
              "primary": {
                  "file": f"logo.{best.ext()}",
                  "source": best.source,
                  "score": round(best.score, 2),
                  **best.info,
              },
              "alternates_kept": min(len(rest), args.keep_alts),
          }
          if args.json:
              print(json.dumps(result, indent=2, ensure_ascii=False))
          else:
              p = result["primary"]
              print(f"✓ {brand_name} → {brand_dir}")
              print(f"  primary: {p['file']}  ({p.get('width')}x{p.get('height')}, "
                    f"alpha={p['has_alpha']}, src={p['source']}, score={p['score']})")
              if result["alternates_kept"]:
                  print(f"  +{result['alternates_kept']} alternates")
          return 0
      
      
      if __name__ == "__main__":
          sys.exit(main())
      
  • .gitignore 78 B · in bundle
  • CHANGELOG.md 484 B
    # Changelog
    
    All notable changes to this skill are documented here.
    Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning: [SemVer](https://semver.org/)
    
    ## [0.3.0] - 2026-08-24
    
    ### Added
    
    - add the shared feedback-classification and approval-invalidation gate used by every LovStudio Skill
    
    ## [0.2.0] - 2026-05-07
    
    ### Added
    
    - add configurable logo archive output
    - standardize install and script invocation examples
    - support SKILL_FIND_LOGO_OUTPUT_DIR
    
    
  • README.md 3 KB
    # Logo 雷达 · Logo Radar
    
    ![Version](https://img.shields.io/badge/version-0.3.0-CC785C)
    
    Fetch a brand/product logo from public sources (Clearbit, og:image, favicon),
    score candidates (prefer wide-aspect + transparent), and archive the winner
    plus alternates under `~/.skill-publisher/logo-collection/<slug>/`.
    
    Useful for building website partner strips, PPT footer rows, poster
    credits — anywhere you need a lineup of logos that look consistent.
    
    Part of [skills](https://example.com/skills/skills) — by [example.com](https://example.com)
    
    ## Install
    
    ```bash
    npx skills add find-logo -g -y
    ```
    
    Stdlib only — no `pip install` needed.
    
    ## Usage
    
    ```bash
    # by name
    python3 scripts/find_logo.py --name "Anthropic"
    
    # by URL (more reliable for non-.com or ambiguous brands)
    python3 scripts/find_logo.py --name "xAI" --url https://x.ai
    
    # machine-readable output for chaining
    python3 scripts/find_logo.py --url https://stripe.com --json
    ```
    
    ## Options
    
    | Option | Default | Description |
    |--------|---------|-------------|
    | `--name` | — | Brand/product name. Used for slug + meta. |
    | `--url` | — | Official URL or bare domain. Overrides the name-based guess. |
    | `--slug` | slugified name | Override the archive directory slug. |
    | `--out` | `SKILL_FIND_LOGO_OUTPUT_DIR` or `~/.skill-publisher/logo-collection` | Archive root. |
    | `--keep-alts` | `2` | How many runner-up candidates to keep. |
    | `--json` | off | Emit JSON to stdout. |
    
    At least one of `--name` or `--url` is required.
    
    ## What gets archived
    
    ```
    ~/.skill-publisher/logo-collection/<slug>/
    ├── logo.<ext>      # highest-scoring candidate
    ├── alt-1.<ext>     # runner-ups (count = --keep-alts)
    ├── alt-2.<ext>
    └── meta.json       # brand, source URL per candidate, dims, format, alpha, score
    ```
    
    ## How candidates are scored
    
    Higher is better. Intended to bubble up **wide-aspect + transparent** logos
    so they line up cleanly next to each other.
    
    | Factor | Points |
    |--------|--------|
    | Format SVG / PNG / WebP / JPG / ICO | +40 / +20 / +10 / -10 / -20 |
    | Has alpha (transparent) | +30 |
    | Aspect ≥ 2:1 (banner) | +25 |
    | Aspect ≥ 1.3:1 (landscape) | +10 |
    | Aspect ≈ 1:1 (square) | -5 |
    | Aspect tall / portrait | -15 |
    | Short edge ≥ 128 / ≥ 64 / < 32 px | +15 / +5 / -20 |
    | Payload < 400 bytes (stub) | -30 |
    
    ## Sources probed, in order
    
    1. **Clearbit Logo API** — `https://logo.clearbit.com/<domain>` (unauthenticated tier).
    2. **og:image / twitter:image + `<link rel="icon">`** — scraped from the brand's homepage. Usually wins because sites ship a proper share card.
    3. **Google s2 favicon** — `https://www.google.com/s2/favicons?domain=<domain>&sz=256` as a safety net.
    
    If all three fail, the script exits `2` with `status: "no-candidates"`. The
    calling agent is expected to fall back to a web search for a press-kit page.
    
    ## User Configuration
    
    Archive files default to `~/.skill-publisher/logo-collection/`. Override per call with
    `--out`, or set `SKILL_FIND_LOGO_OUTPUT_DIR`.
    
    ## License
    
    MIT
    
  • SKILL.md 6.2 KB
    ---
    name: lov-find-logo
    description: >
      Fetch a company/product logo from public sources (Clearbit, og:image,
      favicon) given a brand name or URL, score candidates (wide-aspect +
      transparent preferred), and archive the best + runner-ups to
      the configured logo collection directory.
      Trigger when the user says "find logo", "找 logo", "抓 logo",
      "收集 logo", "brand asset", "需要 <brand> 的 logo",
      or wants logos laid out for a website/PPT/poster.
    license: MIT
    compatibility: >
      Requires Python 3.8+ (stdlib only — no pip deps).
      Cross-platform: macOS, Windows, Linux.
    metadata:
      author: contributors
      version: "0.3.0"
      tags: [branding, assets, logo, scraping]
    ---
    
    # Logo 雷达 · Logo Radar
    
    Takes a brand name or URL, probes Clearbit + the site's own og:image /
    `<link rel=icon>` / favicon, scores each candidate, and archives the best
    one plus a couple of alternates into the configured collection directory.
    
    ## When to Use
    
    - User asks to collect one or more brand logos for a slide/poster/site lineup
    - User names companies to drop into a partners/press strip
    - User gives a URL and wants its logo pulled down cleanly
    
    ## Workflow (MANDATORY)
    
    ### Step 1: Identify each brand
    
    Accept any mix of names and URLs. If the user gave only a name with no obvious
    domain, ask — don't silently guess `.com` (script will guess, but for non-US or
    ambiguous brands that fails).
    
    Use `AskUserQuestion` when:
    - Brand name is ambiguous (e.g. "Apple" = fruit vs. Inc.)
    - No URL and the domain isn't guessable (`xAI` → `x.ai`, not `xai.com`)
    - User gave a list without URLs
    
    ### Step 2: Fetch — one brand per invocation
    
    ```bash
    python3 scripts/find_logo.py --name "Anthropic" --url https://anthropic.com --json
    ```
    
    For a batch, loop; the script is idempotent per `<slug>/` (re-runs overwrite).
    
    ### Step 3: Inspect score; fall back to WebSearch if needed
    
    - Exit code `0` → logo archived. The printed `score` is your quality signal:
      - `≥ 60` — solid: SVG or transparent PNG with wide/square aspect
      - `20–60` — usable: probably a favicon or small PNG
      - `< 20` — weak: only ICO or tiny stub found
    - Exit code `2` / `status: "no-candidates"` → script found nothing.
      Do NOT give up. Use `WebSearch` for `"<brand> logo svg site:*.com"` or the
      brand's press-kit page, then re-invoke with `--url <direct-image-url>` is
      **not supported** — if you have a direct image URL, save it into the configured
      collection directory under `<slug>/logo.<ext>` and hand-write `meta.json`
      using the existing layout as a template.
    
    ### Step 4: Report
    
    Report back with the archive path and the primary's aspect + format. If the
    score is weak, tell the user and offer to retry with a specific press-kit URL
    or Wikipedia SVG.
    
    ## CLI Reference
    
    | Argument | Default | Description |
    |----------|---------|-------------|
    | `--name` | — | Brand/product name. Used for slug + meta. |
    | `--url` | — | Official URL or bare domain. Overrides the name-based domain guess. |
    | `--slug` | slugified name | Override the directory slug under the archive root. |
    | `--out` | `SKILL_FIND_LOGO_OUTPUT_DIR` or `~/.skill-publisher/logo-collection` | Archive root. |
    | `--keep-alts` | `2` | How many runner-up candidates to keep as `alt-N.<ext>`. |
    | `--json` | off | Emit a JSON result to stdout (use this when chaining). |
    
    At least one of `--name` or `--url` is required.
    
    ## Archive Layout
    
    ```
    ~/.skill-publisher/logo-collection/
    ├── anthropic/
    │   ├── logo.png            # primary (highest score)
    │   ├── alt-1.png           # runner-ups
    │   ├── alt-2.png
    │   └── meta.json           # sources, scores, dimensions, fetched_at
    ├── vercel/
    │   ├── logo.png            # 1200x628 transparent banner
    │   └── ...
    └── stripe/
        ├── logo.svg
        └── ...
    ```
    
    ## Scoring Heuristic (why a candidate wins)
    
    - Format: SVG (+40) > PNG (+20) > WebP (+10) > JPG (-10) > ICO (-20)
    - Transparency: `+30` if alpha channel present (SVG always counts)
    - Aspect ratio: `+25` for wide (≥2:1), `+10` for landscape (≥1.3:1),
      `-5` for square, `-15` for tall/portrait
    - Short edge: `+15` if ≥128px, `+5` if ≥64px, `-20` if <32px
    - Size sanity: `-30` if payload <400 bytes (almost certainly a stub)
    
    This matches the "prefer 长条形 + rgba" preference — wide transparent logos
    come out on top, square favicons land as alternates.
    
    ## Dependencies
    
    Stdlib only (urllib, html.parser, argparse). No `pip install` required.
    
    ## User Configuration
    
    Default archive files live under `~/.skill-publisher/logo-collection/`. Override this
    per run with `--out`, or set `SKILL_FIND_LOGO_OUTPUT_DIR` for the skill.
    
    ## Known Limits
    
    - The name → domain guess is a crude lowercase-strip + `.com` suffix. For
      anything not on `.com`, pass `--url` explicitly.
    - No Clearbit API key is used — we hit the unauthenticated endpoint, which
      covers most major brands but not all.
    - `WebSearch` fallback is Claude's responsibility, not the script's.
    
    ## Runtime context (shared)
    
    运行前读取本 Skill 包的 `skill.yaml`,由宿主提供 `skill-runtime/v1` 上下文。字段解析顺序为:当前请求、项目上下文、个人 Preferences、品牌 Profile、通用默认值。
    
    - 只使用 Manifest 声明的字段;Profile 保存公开品牌事实,Preferences 保存个人工作偏好。
    - `required: true` 字段缺失时,按 Manifest 的问题配置向用户提出一个聚焦问题;用户明确同意后再保存回答。
    - 报错提供可复制的 `context_id`、字段路径与来源,诊断内容避开秘密、完整私人路径和原始配置。
    
    ## 通用反馈闭环
    
    用户在 Skill 驱动任务中提出修改意见时,继续当前产物前必须执行:
    
    1. 先判断意见是 `task-specific`(仅本次)还是 `reusable`(可跨任务复用)。
    2. `task-specific` 只修改当前任务,不改 Skill。
    3. `reusable` 先确定作用域:领域规则先更新对应 canonical Skill;适用于所有 Skill 的规则先更新共享规范。
    4. 完成规则更新、版本、lint 与分发核验后,再把修改应用到当前任务。
    5. `reusable` 修改会使此前的“确认”“继续”“发吧”失效;完成当前产物修改和回读后必须停下,等待用户下一步指示,不自动进入发布、提交或其他外部写入。
    
  • skill.yaml 832 B
    schema: skill-manifest/v1
    id: lov-find-logo
    version: "0.3.0"
    runtime: skill-runtime/v1
    context:
      profile:
        fields:
        - path: identity.name
          required: true
          question: 如果本次输出需要品牌身份,请提供品牌名称。
        - path: identity.logo
          required: false
          question: 如果需要使用品牌 Logo,请提供 Logo 地址或文件路径。
        - path: brand.tone
          required: false
          question: 如果已有品牌语气或审美关键词,请提供它们。
      preferences:
        namespace: lov_find_logo
        fields:
        - path: user.language
          required: false
          question: 希望使用哪种语言输出?
        - path: user.timezone
          required: false
          question: 需要使用哪个时区处理日期和时间?
      interaction:
        ask_missing: true
        max_questions: 1
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related