9600dev/mmr/news

Claude Code and agent skill by 9600dev/mmr

Trust tier 2
  • community
LLM review pending
Source check pending

Checking that the SKILL.md file is still in the repository.

Fetch, search, and summarize financial news articles via the local scraper service (~/dev/scraper) at http://127.0.0.1:8089. Handles Cloudflare, paywalls (FT/WSJ/Bloomberg/NYT via archive.ph fallback), and HTML→Markdown extraction. Purpose-built for mmr — includes ticker_news() and portfolio_news() helpers that compose search+scrape for an mmr portfolio or idea scan.

Install

npx vskill@latest install 9600dev/mmr/news

SKILL.md

News Skill (mmr)

LLMVM: Use <helpers>...</helpers> blocks to call NewsHelpers methods. All methods are async. Returns are dicts in the mmr convention — check result.get("ok") and report fetcher_source before trusting content.

This skill wraps a local Dockerized scraper that lives in ~/dev/scraper (renamed from ~/dev/news in mid-2026; config is ~/.config/scraper/scraper.yaml). The service provides browser-style fetching (Playwright + stealth), an httpx → playwright → scrapingbee → archive.ph fallback chain, aggressive HTML→Markdown extraction via rustdown, a persistent searchable scrape store (use_cache, GET /v1/scrapes), server-side image handling (images: urls|hosted|embed), and (optionally) LLM extraction over the scraped body. The generic, always-current description of the service is the scraper repo's own skill at ~/dev/scraper/scraper-service-skill/; the reference/ docs here are copied from it. What this skill adds is the mmr composition layer (ticker_news, portfolio_news, the freshness recipes, FINANCIAL_SOURCES.md).

When to invoke this skill

Trigger when the user:

  • Asks you to fetch / scrape / read / summarize a specific article URL
  • Asks for news on a ticker or portfolio position (use ticker_news() / portfolio_news())
  • Wants a narrative digest on a market catalyst (use search_and_scrape())
  • Hits a paywall, 403, or Cloudflare challenge on a finance source
  • Wants Reuters / Bloomberg / WSJ / FT / CNBC / Seeking Alpha / Stockhead / Motley Fool AU / Market Index article content

Do not invoke for:

  • Raw price/quote lookups — use mmr-skill (MMRHelpers.snapshot, MMRHelpers.history_massive)
  • Fundamentals — use mmr-skill (MMRHelpers.balance_sheet, etc.)
  • Arbitrary non-article URLs (API endpoints, images, GitHub pages)

Service pre-flight: always check health first

python
h = await NewsHelpers.health()
# {"status": "ok", "version": "0.1.0", "llm_enabled": true, "llm_verified": true,
# "mathpix_enabled": true, "scrapingbee_enabled": true, "store_enabled": true,
# "agent_enabled": true, "scrapingbee_usage": {"calls_24h": 0, "credits_24h": 0, ...},
# "degraded_reasons": [], "egress_filter": "enforced"}

status is "ok" only when the core pipeline is up AND no enabled optional subsystem is failing; a non-empty degraded_reasons names the subsystem and its last error. scrapingbee_usage is the paid-tier spend — glance at it before a batch run.

If this returns ok: False with "connection refused", the service isn't running. Start it:

bash
cd ~/dev/scraper && ./docker.sh -g

-g builds (if needed), starts, and tails logs. Wait for scraper service ready on http://... in the log, then re-check health.

NewsHelpers — Method Summary

MethodPurpose
NewsHelpers.health()Ping the service — run before any scrape batch
NewsHelpers.search(query, engine="auto", max_results=10, exclude_regex=None)Search only. Engines: auto, ddg, google, google_news, google_scholar, arxiv, research. Use google_news for freshness + source attribution. Empty result with rate_limited: true = incomplete, retry; missing SerpAPI key = 422, not empty
NewsHelpers.scrape(url, extract=None, follow_links=0, allow_archive_fallback=True, include_html=False, use_cache=False, allow_paid_fallback=True, images=None)Scrape one URL. Optional LLM extraction (requires llm_enabled: true), optional N-hop follow. use_cache=True replays a recent stored result; allow_paid_fallback=False skips the ScrapingBee tier (use on batch sweeps); images="hosted" mirrors images to the service for LLM-visible refs
NewsHelpers.markdown(url)Convenience: returns just the article body markdown string
NewsHelpers.search_and_scrape(query, max_results=5, concurrency=3, engine="google_news")Composite: search + parallel scrape of top N results. Preserves search metadata (source, published_at) per article
NewsHelpers.ticker_news(ticker, max_results=5, extract=None, exchange_hint=None)mmr-native: google_news search "{TICKER} stock [exchange_hint] news" + parallel scrape
NewsHelpers.portfolio_news(tickers, max_results_per_ticker=3, exchange_hint=None)mmr-native: fan out ticker_news() across a list of tickers
NewsHelpers.get_images(url, max_images=5, min_bytes=5000, max_bytes=1_500_000)Extract inline images from an article as llmvm ImageContent (LLM-visible). Filters logos/icons/tracking pixels. og:image / twitter:image are ranked first so the hero photo wins over sidebar promos
NewsHelpers.pdf(url, use_cache=True, timeout=300, images="urls")Convert a PDF to Markdown via Mathpix. The helper forces images="urls" because the service's own /v1/pdf default is "embed" (base64 inline, tens of MB). URL must be public https://; for a local file use POST /v1/convert multipart (see reference/api.md). Returns the same ScrapeResponse shape — article.markdown is the body, article.metadata.pdf_pages / pdf_id / pdf_cached carry diagnostics. /v1/scrape on a .pdf URL transparently delegates here, so most callers won't need to invoke this directly

Core patterns

Pattern 1: Read a specific article
python
res = await NewsHelpers.scrape("https://www.reuters.com/markets/...")
if res.get("ok") and res.get("status") == "ok":
art = res["article"]
print(art["title"])
print(f"fetched via: {art['fetcher_source']}")
print(f"published: {art.get('metadata', {}).get('published_at')}")
print(art["markdown"])

Always inspect `fetcher_source` — if it starts with archive: the content is a snapshot and may be days/weeks/months old. That matters for trading decisions. Mention it to the user.

Pattern 2: News on a specific ticker
python
res = await NewsHelpers.ticker_news("BHP", max_results=5, exchange_hint="ASX")
for a in res["articles"]:
if a.get("ok"):
art = a["article"]
meta = a.get("search", {})
print(f"[{meta.get('source')}] {art['title']}")
print(f" {meta.get('published_at')} · via {art['fetcher_source']}")

exchange_hint disambiguates foreign tickers. Use "ASX", "LSE", "HKEX", "TSE" where relevant. Without it, the query is just "{TICKER} stock news" which biases US-centric.

Pattern 3: News digest for an entire portfolio
python
portfolio = await MMRHelpers.portfolio()
tickers = [p["symbol"] for p in portfolio["data"][:10]]
news = await NewsHelpers.portfolio_news(tickers, max_results_per_ticker=3)
for sym, articles in news["by_ticker"].items():
print(f"\n=== {sym} ===")
for a in articles:
if a.get("ok"):
print(f" - {a['article']['title']}")
Pattern 4: Narrative digest on a macro catalyst
python
bundle = await NewsHelpers.search_and_scrape(
"Fed rate decision 2026 dot plot",
max_results=5,
engine="google_news",
)
# Hand the lot to llm_call for synthesis
corpus = "\n\n---\n\n".join(
a["article"]["markdown"] for a in bundle["articles"] if a.get("ok")
)
digest = await llm_call([corpus], "Summarize the narrative and quote key voices.")
emit(digest)
Pattern 5: Targeted LLM extraction (requires llm_enabled: true on the service)
python
res = await NewsHelpers.scrape(
"https://www.reuters.com/...",
extract="Name the CEO quoted and summarize their statement in one sentence.",
)
if res.get("extraction"):
print(res["extraction"]["text"])
# If the service has llm_enabled=false this returns HTTP 422 with
# error.code == "llm_disabled" — see reference/operations.md to enable.
Pattern 6: Follow-link synthesis (mode 3)
python
res = await NewsHelpers.scrape(
"https://www.reuters.com/markets/weekly-wrap/",
extract="Which central banks moved rates this week and by how much?",
follow_links=3,
)
# The LLM picks up to 3 linked URLs from the seed, scrapes them in parallel,
# and synthesises the extraction across the combined corpus.
print(res.get("extraction", {}).get("text"))
print("followed:", [p["url"] for p in res.get("follow_pages", []) if p.get("ok")])
Pattern 7: Extract images from an article (LLM-visible)

The news-service strips images during HTML→Markdown conversion. Use get_images() when the visual content is part of the story — a chart, a price graph, a map, a product photo, a CEO headshot.

python
res = await NewsHelpers.get_images(
"https://stockhead.com.au/resources/...",
max_images=3, # cap — LLM visual budget is not infinite
min_bytes=5000, # filters out logos, icons, tracking pixels
)
if res["ok"]:
for img in res["images"]:
emit(img) # LLM can now see the image
for m in res["image_meta"]:
print(f" {m['alt']} ({m['bytes']} bytes, {m['content_type']})")

Under the hood this:

  1. Calls scrape(url, include_html=True) to get the cleaned HTML.
  2. Parses <img> tags (including lazy-load variants data-src, data-lazy-src).
  3. Resolves relative URLs, filters ad/analytics hosts, SVGs, GIFs.
  4. Downloads surviving candidates in parallel via httpx.
  5. Drops anything under min_bytes (most logos/icons/favicons are <5KB).
  6. Wraps remaining bytes as llmvm_lite.llm_types.ImageContent objects.

Returns:

python
{
"ok": True,
"article_title": "…",
"article_url": "…", # final URL after redirects
"fetcher_source": "httpx", # or "playwright", "archive:*"
"count": 3,
"images": [ImageContent, ...], # pass to emit() or llm_call()
"image_meta": [{src, alt, bytes, content_type}, ...],
"skipped": [{src, reason}, ...], # why each candidate was dropped
}

Use sparingly. Each image adds meaningful tokens to the LLM context (a 1200×675 JPEG is typically 1,500–3,000 visual tokens). 1–3 images per article is usually the sweet spot for market commentary; set max_images=1 if you only need the hero image.

Works via the scrape fallback chain. Since get_images() piggybacks on scrape(include_html=True), it benefits from the same httpx → playwright → archive tier — so it'll still pull images out of Cloudflare-protected or paywalled articles that come through the archive tier.

Passing to `llm_call()` for visual analysis:

python
res = await NewsHelpers.get_images(chart_article_url, max_images=1)
if res["images"]:
analysis = await llm_call(
res["images"] + [res.get("markdown", "")],
"Read the chart in the image and tell me the key price levels.",
)
emit(analysis)

Finding the latest financial news

The server doesn't support date filters directly — fresh news is a function of (a) using `google_news` (only engine that returns published_at), (b) phrasing queries with temporal anchors ("today", "this week", the current month/year), and (c) sorting/filtering on `published_at` client-side after fetching.

Recipe A: latest news for a single ticker

ticker_news() already uses google_news under the hood. For max recency, add a date anchor to the query by passing it through search_and_scrape():

python
import datetime as dt
this_month = dt.datetime.now().strftime("%B %Y") # e.g. "April 2026"
bundle = await NewsHelpers.search_and_scrape(
f"AGE Alligator Energy ASX news {this_month}",
max_results=8,
engine="google_news",
)
# Sort by published_at (most recent first); None values sink to the bottom.
articles = sorted(
[a for a in bundle["articles"] if a.get("ok")],
key=lambda a: a.get("search", {}).get("published_at") or "",
reverse=True,
)
for a in articles[:5]:
s = a["search"]
print(f"{s.get('published_at')} · [{s.get('source')}] {a['article']['title']}")
Recipe B: latest news on an entire sector

For "what's moving in ASX uranium miners this week" style scans:

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewNot runA model review is scheduled for the next rescan.

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
115
Repository forks
36
7-day trend score
24

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/9600dev/mmr/news/badge)](https://verified-skill.com/skills/9600dev/mmr/news)

Something wrong with this skill or its page? Tell us.