
Geeknews Search
- 2.8k installs
- 6.5k repo stars
- Updated July 27, 2026
- nomadamas/k-skill
geeknews-search is a read-only skill listing and searching GeekNews public RSS feed entries via geeknews_search.py CLI.
About
GeekNews Search reads the public GeekNews RSS and Atom feed at feeds.feedburner.com geeknews-feed to list recent posts, search by title summary author or link, and inspect item details read-only. Workflows call python3 scripts geeknews_search.py with list, search, and detail subcommands plus limit and query flags. List mode returns recent entries; search matches conservatively on title, summary, author, and link or id fields; detail fetches RSS content and original article links by id or topic number. No authentication is required and v1 stays RSS-first without unofficial APIs or login sessions. Offline testing supports --feed-file with saved Atom XML. Failure modes note FeedBurner outages, truncated HTML summaries, and missing full article comments or vote data beyond feed scope. Done criteria include showing latest headlines, organizing keyword search results with title link author summary, and presenting RSS-based detail with source links. Locale is ko-KR with GeekNews home at news.hada.io as the official surface reference.
- Read-only RSS feed list, search, and detail commands.
- geeknews_search.py list search detail CLI workflows.
- Conservative title summary author link search matching.
- Optional --feed-file for offline Atom XML testing.
- No auth, unofficial API, or login session dependency.
Geeknews Search by the numbers
- 2,812 all-time installs (skills.sh)
- +127 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #45 of 826 Skill Development skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
geeknews-search capabilities & compatibility
- Capabilities
- recent geeknews entry listing with limit · conservative keyword search across feed fields · detail lookup by id or link fragment · offline atom xml feed file testing mode · rss scope boundaries and failure mode docs · original article link presentation with summarie
- Use cases
- research · web search
- Pricing
- Free
What geeknews-search says it does
v1은 RSS-first, read-only 범위다.
검색은 제목, 요약, 작성자, 링크/id 기준으로만 동작한다.
비공식 API나 로그인 세션에 의존하지 않는다.
npx skills add https://github.com/nomadamas/k-skill --skill geeknews-searchAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.8k |
|---|---|
| repo stars | ★ 6.5k |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 27, 2026 |
| Repository | nomadamas/k-skill ↗ |
How do I find today's GeekNews posts or search Korean tech news from the public feed?
List, search, and inspect GeekNews posts from the public RSS feed with read-only CLI scripts.
Who is it for?
Users monitoring GeekNews headlines or searching feed topics without scraping the website.
Skip if: Skip for full article bodies, comments, votes, or authenticated GeekNews features outside RSS.
When should I use this skill?
User asks about GeekNews today posts, keyword search in GeekNews, or RSS item detail by id.
What you get
Structured list, search results, or RSS-based detail with title, author, summary, and original links.
- GeekNewsItem JSON records
- Parsed headline and article text
Files
GeekNews Search
What this skill does
GeekNews 공개 RSS/Atom 피드(https://feeds.feedburner.com/geeknews-feed)를 사용해 최신 글을 읽기 전용으로 조회한다.
- 최신 글 목록 조회
- 제목/요약/작성자 기준 검색
- 항목 id/link 기준 상세 확인
When to use
- "긱뉴스 오늘 뭐 올라왔어?"
- "긱뉴스에서 Claude 관련 글 찾아줘"
- "이 GeekNews 글 요약/링크 확인해줘"
Inputs
- 기본: 별도 인증 없이 public feed만 사용
- 목록 조회:
limit - 검색:
query, 선택limit - 상세 조회:
id또는 링크/토픽 번호 일부
Official surface
- GeekNews RSS/Atom feed:
https://feeds.feedburner.com/geeknews-feed - GeekNews home:
https://news.hada.io
Workflow
1) List recent entries
python3 scripts/geeknews_search.py list --limit 102) Search the feed conservatively
python3 scripts/geeknews_search.py search --query Claude --limit 5검색은 제목, 요약, 작성자, 링크/id 기준으로만 동작한다.
3) Inspect a specific item
python3 scripts/geeknews_search.py detail --id 28439상세 조회는 RSS 피드에 포함된 content/요약과 원문 링크를 함께 돌려준다.
Done when
- 최신 GeekNews 글 목록을 바로 보여줄 수 있다.
- 키워드 검색 결과에서 제목/링크/작성자/요약을 정리할 수 있다.
- 특정 항목의 RSS 기반 내용을 보수적으로 확인하고 원문 링크를 함께 제시할 수 있다.
Failure modes
- FeedBurner/GeekNews feed가 일시적으로 응답하지 않을 수 있다.
- RSS 피드가 제공하는 범위를 넘는 전체 본문/댓글/투표 정보는 포함되지 않는다.
- HTML 요약은 feed 원문 기준이라 일부가 잘릴 수 있다.
Notes
- v1은 RSS-first, read-only 범위다.
- 비공식 API나 로그인 세션에 의존하지 않는다.
- 테스트/오프라인 검증 시
--feed-file로 저장된 Atom XML을 넣을 수 있다.
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import re
import urllib.request
from dataclasses import asdict, dataclass
from html import unescape
from html.parser import HTMLParser
from pathlib import Path
GEEKNEWS_FEED_URL = "https://feeds.feedburner.com/geeknews-feed"
class _TextExtractor(HTMLParser):
def __init__(self) -> None:
super().__init__()
self.parts: list[str] = []
def handle_data(self, data: str) -> None:
self.parts.append(data)
def text(self) -> str:
return " ".join(part.strip() for part in self.parts if part.strip())
@dataclass(frozen=True)
class GeekNewsItem:
id: str
title: str
link: str
published: str | None
updated: str | None
author_name: str | None
author_url: str | None
summary: str
content_html: str
def to_dict(self) -> dict[str, object]:
return asdict(self)
@dataclass(frozen=True)
class GeekNewsFeed:
title: str
source_id: str | None
updated: str | None
home_url: str | None
feed_url: str | None
category: str | None
items: list[GeekNewsItem]
def source_dict(self) -> dict[str, object]:
return {
"title": self.title,
"id": self.source_id,
"updated": self.updated,
"home_url": self.home_url,
"feed_url": self.feed_url,
"category": self.category,
}
def _strip_cdata(value: str | None) -> str:
if not value:
return ""
stripped = value.strip()
if stripped.startswith("<![CDATA[") and stripped.endswith("]]>"):
return stripped[9:-3]
return stripped
def _collapse_whitespace(value: str) -> str:
return re.sub(r"\s+", " ", value).strip()
def _clean_xml_text(value: str | None) -> str:
return _collapse_whitespace(unescape(_strip_cdata(value)))
def _html_to_text(html: str) -> str:
parser = _TextExtractor()
parser.feed(html)
parser.close()
return _collapse_whitespace(unescape(parser.text()))
def _first_tag(block: str, tag: str) -> str | None:
match = re.search(rf"<{tag}\b[^>]*>(.*?)</{tag}>", block, re.DOTALL)
if not match:
return None
return _clean_xml_text(match.group(1))
def _first_raw_tag(block: str, tag: str) -> str | None:
match = re.search(rf"<{tag}\b[^>]*>(.*?)</{tag}>", block, re.DOTALL)
if not match:
return None
return _strip_cdata(match.group(1)).strip()
def _first_link_href(block: str) -> str | None:
patterns = (
r"<link\b[^>]*rel=['\"]alternate['\"][^>]*href=['\"]([^'\"]+)['\"]",
r"<link\b[^>]*href=['\"]([^'\"]+)['\"]",
)
for pattern in patterns:
match = re.search(pattern, block)
if match:
return unescape(match.group(1).strip())
return None
def _link_href(block: str, *, rel: str | None = None) -> str | None:
if rel:
match = re.search(
rf"<link\b[^>]*(?:rel|ref)=['\"]{re.escape(rel)}['\"][^>]*href=['\"]([^'\"]+)['\"]",
block,
)
if match:
return unescape(match.group(1).strip())
return _first_link_href(block)
def _feed_prefix(xml_text: str) -> str:
if "<entry" not in xml_text:
return xml_text
return xml_text.split("<entry", 1)[0]
def _entry_blocks(xml_text: str) -> list[str]:
return re.findall(r"<entry\b[^>]*>(.*?)</entry>", xml_text, re.DOTALL)
def _validate_limit(limit: int) -> int:
if limit <= 0:
raise ValueError("limit must be positive")
return limit
def load_feed(xml_text: str) -> GeekNewsFeed:
prefix = _feed_prefix(xml_text)
items = []
for entry in _entry_blocks(xml_text):
author_block_match = re.search(r"<author\b[^>]*>(.*?)</author>", entry, re.DOTALL)
author_block = author_block_match.group(1) if author_block_match else ""
content_html = (_first_raw_tag(entry, "content") or "").strip()
items.append(
GeekNewsItem(
id=_first_tag(entry, "id") or "",
title=_first_tag(entry, "title") or "",
link=_first_link_href(entry) or (_first_tag(entry, "id") or ""),
published=_first_tag(entry, "published") or _first_tag(entry, "updated"),
updated=_first_tag(entry, "updated"),
author_name=_first_tag(author_block, "name"),
author_url=_first_tag(author_block, "uri"),
summary=_html_to_text(content_html),
content_html=content_html,
)
)
category_match = re.search(r"<category\b[^>]*term=['\"]([^'\"]+)['\"]", prefix)
return GeekNewsFeed(
title=_first_tag(prefix, "title") or "GeekNews",
source_id=_first_tag(prefix, "id"),
updated=_first_tag(prefix, "updated"),
home_url=_link_href(prefix, rel="alternate"),
feed_url=_link_href(prefix, rel="self") or _first_tag(prefix, "id"),
category=category_match.group(1) if category_match else None,
items=items,
)
def list_items(feed: GeekNewsFeed, limit: int = 10) -> list[GeekNewsItem]:
return feed.items[:_validate_limit(limit)]
def search_items(feed: GeekNewsFeed, query: str, limit: int = 10) -> list[GeekNewsItem]:
if not query.strip():
raise ValueError("query is required")
limit = _validate_limit(limit)
needle = query.casefold()
matches = []
for item in feed.items:
haystack = "\n".join(
part
for part in (
item.title,
item.summary,
item.author_name or "",
item.author_url or "",
item.id,
item.link,
)
if part
).casefold()
if needle in haystack:
matches.append(item)
if len(matches) >= limit:
break
return matches
def get_item_detail(feed: GeekNewsFeed, lookup: str) -> GeekNewsItem:
normalized_lookup = lookup.strip().casefold()
if not normalized_lookup:
raise ValueError("lookup is required")
for item in feed.items:
candidates = [item.id, item.link, item.title]
lowered = [candidate.casefold() for candidate in candidates if candidate]
if normalized_lookup in lowered or any(normalized_lookup in candidate for candidate in lowered):
return item
raise LookupError(f"No GeekNews entry matched: {lookup}")
def _serialize_items(items: list[GeekNewsItem]) -> list[dict[str, object]]:
return [item.to_dict() for item in items]
def build_list_payload(feed: GeekNewsFeed, limit: int = 10) -> dict[str, object]:
items = list_items(feed, limit=limit)
return {"source": feed.source_dict(), "count": len(items), "items": _serialize_items(items)}
def build_search_payload(feed: GeekNewsFeed, query: str, limit: int = 10) -> dict[str, object]:
items = search_items(feed, query=query, limit=limit)
return {
"source": feed.source_dict(),
"query": query,
"count": len(items),
"items": _serialize_items(items),
}
def build_detail_payload(feed: GeekNewsFeed, lookup: str) -> dict[str, object]:
item = get_item_detail(feed, lookup)
return {"source": feed.source_dict(), "item": item.to_dict()}
def fetch_feed(url: str = GEEKNEWS_FEED_URL, timeout: int = 20) -> str:
request = urllib.request.Request(url, headers={"User-Agent": "k-skill-geeknews/1.0"})
with urllib.request.urlopen(request, timeout=timeout) as response:
charset = response.headers.get_content_charset() or "utf-8"
return response.read().decode(charset, errors="replace")
def _add_feed_source_args(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--feed-url", default=GEEKNEWS_FEED_URL, help="기본값: GeekNews public feed URL")
parser.add_argument("--feed-file", help="테스트/오프라인 검증용 로컬 Atom XML 파일")
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Read GeekNews entries from the public RSS/Atom feed.")
subparsers = parser.add_subparsers(dest="command", required=True)
list_parser = subparsers.add_parser("list", help="최신 GeekNews 항목 목록")
_add_feed_source_args(list_parser)
list_parser.add_argument("--limit", type=int, default=10)
search_parser = subparsers.add_parser("search", help="제목/요약/작성자 기준 검색")
_add_feed_source_args(search_parser)
search_parser.add_argument("--query", required=True)
search_parser.add_argument("--limit", type=int, default=10)
detail_parser = subparsers.add_parser("detail", help="항목 상세 확인")
_add_feed_source_args(detail_parser)
detail_parser.add_argument("--id", required=True, help="entry id/link/topic id 일부")
return parser.parse_args(argv)
def _load_feed_text(args: argparse.Namespace) -> str:
if args.feed_file:
return Path(args.feed_file).read_text(encoding="utf-8")
return fetch_feed(url=args.feed_url)
def main(argv: list[str] | None = None) -> None:
args = parse_args(argv)
feed = load_feed(_load_feed_text(args))
if args.command == "list":
payload = build_list_payload(feed, limit=args.limit)
elif args.command == "search":
payload = build_search_payload(feed, query=args.query, limit=args.limit)
else:
payload = build_detail_payload(feed, lookup=args.id)
print(json.dumps(payload, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
Related skills
How it compares
Pick geeknews-search for GeekNews-specific developer headlines; use general Hacker News skills for Y Combinator forum content.
FAQ
Does search include full article text?
No. Search matches title, summary, author, and link or id only within RSS scope.
Offline testing supported?
Yes. Pass --feed-file with saved Atom XML instead of live FeedBurner fetch.
Authentication required?
No. v1 uses the public feed only with no login session.
Is Geeknews Search safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.