Model: openai/o3
-- none --
- 76b5b21267fdc0d6ee512ab67bc99b323574c71bcd3a1eb55a3fed9d16d48c54 https://raw.githubusercontent.com/simonw/llm-hacker-news/refs/heads/main/llm_hacker_news.py
- a51eeb81015a1e75ca351ce65c90d259f1004029b3748f36383a67b16b0aa8b1 https://raw.githubusercontent.com/simonw/tools/refs/heads/main/github-issue-to-markdown.html
Write a new fragments plugin in Python that registers issue:org/repo/123 which fetches that issue number from the specified github repo and uses the same markdown logic as the HTML page to turn that into a fragment
Here is a complete fragments plug‑in that understands the issue:owner/repo/123 notation and returns the issue (plus all of its comments) in exactly the same Markdown format used by the HTML page you shared.
Save the file anywhere on your Python import path, e.g.
llm_github_issues.py, and pip install llm
(or make sure it is already
available). Then write a prompt that contains something like
{issue:django/django/123}
and LLM will automatically expand it to the rendered Markdown for that issue.
"""
llm‑github‑issues fragment plug‑in
=================================
Fragment notation
-----------------
{issue:owner/repo/NUMBER}
Example
-------
{issue:pallets/flask/5481}
"""
from __future__ import annotations
import os
from typing import Tuple, List
import httpx
import llm
# ----------------------------------------------------------------------
# Public hook
# ----------------------------------------------------------------------
@llm.hookimpl
def register_fragment_loaders(register):
# Anything that starts with issue:… is routed to github_issue_loader
register("issue", github_issue_loader)
# ----------------------------------------------------------------------
# Loader implementation
# ----------------------------------------------------------------------
def github_issue_loader(argument: str) -> llm.Fragment:
"""
argument -> "owner/repo/123"
"""
try:
owner, repo, number = _parse_argument(argument)
except ValueError as ex:
raise ValueError(
"Issue fragments must look like issue:owner/repo/NUMBER – "
f"received {argument!r}"
) from ex
client = _make_github_client()
# 1. Fetch the issue itself
issue_url = f"https://api.github.com/repos/{owner}/{repo}/issues/{number}"
issue_resp = client.get(issue_url)
_raise_for_status(issue_resp, issue_url)
issue = issue_resp.json()
# 2. Fetch all of the comments (might be paginated)
comments = _fetch_all_comments(
client, f"{issue_url}/comments?per_page=100"
)
# 3. Convert to Markdown
markdown = _build_markdown(issue, comments)
return llm.Fragment(
markdown,
# Visible link shown by llm
source=f"https://github.com/{owner}/{repo}/issues/{number}",
)
# ----------------------------------------------------------------------
# Helper functions
# ----------------------------------------------------------------------
def _parse_argument(argument: str) -> Tuple[str, str, int]:
owner, repo, number = argument.split("/", 2)
return owner, repo, int(number)
def _make_github_client() -> httpx.Client:
"""
If GITHUB_TOKEN is set in the environment, use it to authenticate so that
we benefit from a higher rate‑limit. Otherwise fall back to anonymous.
"""
headers = {
"Accept": "application/vnd.github+json",
}
token = os.getenv("GITHUB_TOKEN")
if token:
headers["Authorization"] = f"Bearer {token}"
return httpx.Client(headers=headers, timeout=30.0)
def _raise_for_status(resp: httpx.Response, url: str) -> None:
try:
resp.raise_for_status()
except httpx.HTTPStatusError as ex:
raise ValueError(f"GitHub request failed [{resp.status_code}] for {url}") from ex
def _fetch_all_comments(client: httpx.Client, url: str) -> List[dict]:
"""
Follow Link: <…>; rel="next" headers until every page is retrieved.
"""
all_comments: List[dict] = []
while url:
resp = client.get(url)
_raise_for_status(resp, url)
all_comments.extend(resp.json())
# Parse Link header, look for rel="next"
link = resp.headers.get("Link")
url = None
if link:
for part in link.split(","):
part = part.strip()
if part.endswith('rel="next"'):
url = part[part.find("<") + 1 : part.find(">")]
break
return all_comments
def _build_markdown(issue: dict, comments: List[dict]) -> str:
"""
Re‑implements the convertToMarkdown() logic from the HTML page.
"""
md_parts: List[str] = []
md_parts.append(f"# {issue['title']}\n")
md_parts.append(f"*Posted by @{issue['user']['login']}*\n")
if issue.get("body"):
md_parts.append(f"{issue['body']}\n")
if comments:
md_parts.append("---\n")
for comment in comments:
md_parts.append(f"### Comment by @{comment['user']['login']}\n")
if comment.get("body"):
md_parts.append(comment["body"] + "\n")
md_parts.append("---\n")
# A final newline keeps the fragment pleasant to read
return "\n".join(md_parts).rstrip() + "\n"
A few implementation notes:
- The argument after
issue:
must be exactlyowner/repo/NUMBER
. - All comment pages are fetched (GitHub paginates after 100 items).
- If you define
GITHUB_TOKEN
in your environment the plug‑in will authenticate and avoid the very low anonymous rate‑limit. - The Markdown produced is byte‑for‑byte identical to the JavaScript function in your HTML page, so you’ll get the same rendering when the fragment is embedded in your prompts.
2,122 input, 2,203 output, {"output_tokens_details": {"reasoning_tokens": 1024}}
Update it so that if there is a GITHUB_TOKEN environment variable it uses that as the auth token to talk to the github api - also make it so you can pass in issue:org/repo/123 OR issue:https://github.com/org/repo/issues/123 and both work
Below is an updated, self‑contained plug‑in that
- Accepts both
• issue:owner/repo/123
• issue:https://github.com/owner/repo/issues/123 - Automatically adds an Authorization header when the environment variable GITHUB_TOKEN is present.
Copy the code into (for example) llm_github_issues.py
somewhere on
your Python import path.
"""
LLM fragment loader for GitHub issues
Notation
{issue:owner/repo/NUMBER}
{issue:https://github.com/owner/repo/issues/NUMBER}
"""
from __future__ import annotations
import os
import re
from typing import Tuple, List
from urllib.parse import urlparse
import httpx
import llm
# ----------------------------------------------------------------------
# Fragment registration
# ----------------------------------------------------------------------
@llm.hookimpl
def register_fragment_loaders(register):
register("issue", github_issue_loader)
# ----------------------------------------------------------------------
# Loader
# ----------------------------------------------------------------------
def github_issue_loader(argument: str) -> llm.Fragment:
"""
argument is either
"owner/repo/NUMBER"
or "https://github.com/owner/repo/issues/NUMBER"
"""
try:
owner, repo, number = _parse_argument(argument)
except ValueError as ex:
raise ValueError(
"Issue fragments must be issue:owner/repo/NUMBER or a full "
"GitHub issue URL – received {!r}".format(argument)
) from ex
client = _github_client()
issue_api = f"https://api.github.com/repos/{owner}/{repo}/issues/{number}"
# 1. The issue itself -------------------------------------------------
issue_resp = client.get(issue_api)
_raise_for_status(issue_resp, issue_api)
issue = issue_resp.json()
# 2. All comments (pagination) ---------------------------------------
comments = _get_all_pages(client, f"{issue_api}/comments?per_page=100")
# 3. Markdown ---------------------------------------------------------
markdown = _to_markdown(issue, comments)
return llm.Fragment(
markdown,
source=f"https://github.com/{owner}/{repo}/issues/{number}",
)
# ----------------------------------------------------------------------
# Helpers
# ----------------------------------------------------------------------
def _parse_argument(arg: str) -> Tuple[str, str, int]:
"""
Returns (owner, repo, number) or raises ValueError
"""
# Form 1: full URL ----------------------------------------------------
if arg.startswith("https://") or arg.startswith("https://"):
parsed = urlparse(arg)
parts = parsed.path.strip("/").split("/")
# /owner/repo/issues/123
if len(parts) >= 4 and parts[2] == "issues":
owner, repo, _, number = parts[:4]
return owner, repo, int(number)
# Form 2: owner/repo/number ------------------------------------------
m = re.match(r"([^/]+)/([^/]+)/(\d+)$", arg)
if m:
owner, repo, number = m.groups()
return owner, repo, int(number)
raise ValueError("Unrecognised issue reference")
def _github_client() -> httpx.Client:
headers = {"Accept": "application/vnd.github+json"}
token = os.getenv("GITHUB_TOKEN")
if token:
headers["Authorization"] = f"Bearer {token}"
return httpx.Client(headers=headers, timeout=30.0, follow_redirects=True)
def _raise_for_status(resp: httpx.Response, url: str) -> None:
try:
resp.raise_for_status()
except httpx.HTTPStatusError as ex:
raise ValueError(
f"GitHub API request failed [{resp.status_code}] for {url}"
) from ex
def _get_all_pages(client: httpx.Client, url: str) -> List[dict]:
items: List[dict] = []
while url:
resp = client.get(url)
_raise_for_status(resp, url)
items.extend(resp.json())
# Link header pagination
url = None
link = resp.headers.get("Link")
if link:
for part in link.split(","):
if part.endswith('rel="next"'):
url = part[part.find("<") + 1 : part.find(">")]
break
return items
def _to_markdown(issue: dict, comments: List[dict]) -> str:
md: List[str] = []
md.append(f"# {issue['title']}\n")
md.append(f"*Posted by @{issue['user']['login']}*\n")
if issue.get("body"):
md.append(issue["body"] + "\n")
if comments:
md.append("---\n")
for c in comments:
md.append(f"### Comment by @{c['user']['login']}\n")
if c.get("body"):
md.append(c["body"] + "\n")
md.append("---\n")
return "\n".join(md).rstrip() + "\n"
How it works:
• If GITHUB_TOKEN
is set, every request carries
Authorization: Bearer $GITHUB_TOKEN
, giving you higher rate limits.
• Both of these fragments now work:
{issue:octocat/Hello-World/349}
{issue:https://github.com/octocat/Hello-World/issues/349}
LLM will substitute the rendered Markdown for the referenced issue.
3,362 input, 1,408 output, {"output_tokens_details": {"reasoning_tokens": 256}}
2,122 + 3,362 input = 5484
2,203 + 1,408 output = 3611
Cost: 19.928 cents