This directory contains all data source extensions. Each extension is a self-contained unit that owns its full pipeline:
fetch() β process() β render() β FeedSection
The orchestrator (main.py) calls ext.run() on every enabled extension and assembles the results into the daily payload.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BaseExtension.run() β
β β
β 1. fetch() β pull raw items (no LLM) β
β 2. process() β score / filter / summarise (LLM ok) β
β 3. render() β package into FeedSection β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The output every extension must produce:
@dataclass
class FeedSection:
key: str # snake_case, matches config/sources.yaml key
title: str # display name shown in rendered output
icon: str # marker shown in nav + heading
payload_key: str | None # optional flat JSON key (defaults to key)
items: list[dict] # processed item dicts
meta: dict # optional stats (counts, durations, etc.)The orchestrator merges your sources.yaml block with config/extensions/{name}.yaml and injects:
| Key | Value |
|---|---|
enabled |
bool from sources.yaml |
language |
output language code (e.g. "en") |
llm_scoring_model |
model name for scoring |
llm_summarization_model |
model name for summarisation |
dry_run |
True when --dry-run flag is set β skip all LLM calls |
cp -r extensions/_template extensions/my_sourceEdit extensions/my_source/meta.json.
This file is the source of truth for the Astro-side extension registry used by:
- the setup wizard source picker
- the setup wizard field forms
- section titles / subtitles / layout hints in the site
After editing metadata, refresh the generated Astro registry:
cd astro
npm run sync:extension-metaOpen extensions/my_source/__init__.py and implement:
fetch()β pull raw data. Return a list of dicts. No LLM calls here.process()β optional. Callself.llmto score or summarise. Respectdry_run.render()β wrap items in aFeedSection. No network calls here.
In extensions/__init__.py:
from extensions.my_source import MySourceExtension
REGISTRY = [
...,
MySourceExtension, # add here
]Add it to config/sources.yaml:
display_order:
- my_sourcemy_source:
enabled: true
# any source-level limits (e.g. max_items)If your extension needs filter/keyword config, create config/extensions/my_source.yaml:
# config/extensions/my_source.yaml
keywords:
- AI
- machine learning
llm_score_threshold: 7Add tests/test_my_source.py. At minimum, test your fetch() parsing logic with a fixture β no live network calls needed. See tests/test_hn_collector.py for a simple example.
# Run all tests
PYTHONPATH=. pytest tests/ -q
# Run only your extension's tests
PYTHONPATH=. pytest tests/test_my_source.py -v
# Smoke test: fetch real data without LLM calls (no API cost)
python main.py --dry-run--dry-run fetches live data from all enabled extensions but skips every LLM call, so you can verify your fetch() and parsing logic without spending any API credits.
Each extension is a package (extensions/<name>/) containing __init__.py (the extension class) and usually README.md (docs specific to that extension).
Each built-in extension also has a meta.json file that feeds the Astro setup wizard registry.
If your extension needs a custom card layout in the Astro site, add a component under astro/src/components/ and register it in SectionBlock.astro. The default GenericCard.astro handles any unknown key automatically.
| Package | Key | What it does |
|---|---|---|
arxiv/ |
arxiv |
Fetches arXiv papers, LLM-scores and summarises them β docs |
hacker_news/ |
hacker_news |
Fetches top HN stories above a score threshold β docs |
github_trending/ |
github_trending |
Fetches daily trending GitHub repos β docs |
us_stocks/ |
us_stocks |
Builds a pre-market signal board for configurable US stock watchlists β docs |
postdoc_jobs/ |
postdoc_jobs |
Fetches and ranks research job postings β docs |
supervisor_updates/ |
supervisor_updates |
Monitors supervisor / lab pages for changes β docs |
weather/ |
weather |
Fetches today's weather for a configured city |
Before opening a PR with a new extension:
-
keyis unique and matchesconfig/sources.yaml -
meta.jsonexists andmeta.json.keymatches the directory name -
fetch()makes no LLM calls -
process()checksself.config.get("dry_run")and skips LLM calls if set -
render()makes no network calls - Credentials read from
os.environonly β never from config files - At least one test covering the parsing/filtering logic
-
PYTHONPATH=. pytest tests/ -qpasses