Skill: ivangdavila/yahoo (version 1.0.0)

--- name: Yahoo slug: yahoo version: 1.0.0 homepage: https://clawic.com/skills/yahoo description: Use Yahoo Finance for quotes, symbol search, watchlists, market briefs, and catalyst-aware stock decisions. changelog: Initial release with Yahoo Finance quote scripts, watchlist briefings, and risk-first decision workflows. metadata: {"clawdbot":{"emoji":"📉","requires":{"bins":["python3"]},"os":["linux","darwin","win32"],"configPaths":["~/yahoo/"]}} --- ## When to Use User needs Yahoo Finance workflows, ticker discovery, quote checks, watchlist reviews, or fast market context before making a stock decision. Agent handles symbol search, single-name snapshots, multi-ticker briefings, and risk-aware thesis framing without pretending to place trades. ## Architecture Memory lives in `~/yahoo/`. If `~/yahoo/` does not exist, run `setup.md`. See `memory-template.md` for structure. ```text ~/yahoo/ ├── memory.md # Activation preferences, market scope, and answer style ├── watchlist.md # Ranked tickers, benchmarks, and catalyst notes ├── briefing-log.md # Saved pre-market, post-market, or event-driven briefs └── decisions.md # Thesis, invalidation, and review notes ``` ## Quick Reference Load only the files that improve the current answer. Use the scripts when deterministic output is faster than manual browsing. | Topic | File | |-------|------| | Setup and activation | `setup.md` | | Local memory structure | `memory-template.md` | | Command recipes | `commands.md` | | Market briefing workflow | `market-playbook.md` | | Thesis framing template | `thesis-card.md` | | Risk guardrails | `risk-playbook.md` | | Single-symbol snapshot script | `yahoo_quote.py` | | Symbol and headline search script | `yahoo_search.py` | | Multi-ticker briefing script | `yahoo_brief.py` | ## Requirements - `python3` - Outbound web access to `finance.yahoo.com` and Yahoo Finance search endpoints - User approval before creating or updating local watchlists or decision logs ## Quick Start ```bash python3 yahoo_search.py "apple" python3 yahoo_quote.py AAPL python3 yahoo_brief.py AAPL MSFT NVDA AMZN ``` Use the scripts for grounded snapshots, then layer judgment with `market-playbook.md`, `thesis-card.md`, and `risk-playbook.md`. ## Operating Coverage ### Fast quote check - Pull the current Yahoo Finance price panel, day move, and headline statistics for one symbol. - Use `yahoo_quote.py` when the user wants a quick tape read before deeper analysis. ### Symbol resolution and headline scan - Resolve ambiguous company names, ETFs, ADRs, and similarly named tickers before analysis. - Use `yahoo_search.py` when the user says "Apple", "Nvidia", "QQQ", or anything that could map to multiple instruments. ### Watchlist and market brief - Compare multiple names side by side, surface leaders and laggards, and flag extreme movers worth a second look. - Use `yahoo_brief.py` when the user wants a pre-market, post-market, or event-driven pass across a basket. ## Core Rules ### 1. Resolve the exact symbol before making claims - Confirm the tradable instrument first: common stock, ADR, ETF, index, crypto pair, or forex pair. - Ambiguous names create fake analysis and wrong catalysts. - Use `yahoo_search.py` whenever the input is not already a clean ticker. ### 2. Match the workflow to the decision horizon - Separate quick quote checks, same-day tape reads, swing planning, and longer position reviews. - The same number means different things on an intraday trade than on a three-month thesis. - Start every answer with the user's time horizon if it changes the recommendation. ### 3. Separate tape, thesis, and trigger - Report what Yahoo shows now, what the thesis claims, and what evidence would validate or break it. - Price action alone is not a thesis. - Use `thesis-card.md` to convert commentary into a falsifiable setup. ### 4. Keep catalyst timing explicit - Mention the nearest catalyst window that could invalidate a calm-looking setup: earnings, macro releases, product events, or regulatory dates. - If there is no clear catalyst, say that the setup is mostly technical or mostly watchlist-only. - Use `market-playbook.md` to frame why the next time window matters. ### 5. Put risk controls ahead of opportunity - Position sizing, invalidation, liquidity, and volatility limits come before upside stories. - A good idea with undefined risk is still a bad trade. - Use `risk-playbook.md` before suggesting size, urgency, or conviction. ### 6. Treat local storage as opt-in continuity - Keep local memory limited to watchlists, recurring benchmarks, preferred answer style, and durable risk boundaries. - Do not create `~/yahoo/` silently for sensitive market context. - Save only what clearly improves the next Yahoo workflow. ### 7. Verify timestamp and market state every time - Read whether the number is at close, pre-market, post-market, or delayed. - Users misread stale prints as live confirmation all the time. - Quote answers must include the market-state context when Yahoo exposes it. ## Common Traps - Analyzing the wrong ticker because the company name matched multiple instruments -> wrong catalyst map and wrong price history. - Treating a daily move as a thesis by itself -> momentum chasing without structure. - Ignoring whether Yahoo is showing close, pre-market, or after-hours data -> stale or misread entries. - Ranking a watchlist without catalyst timing -> busy output with no decision value. - Saving detailed holdings or account balances by default -> unnecessary privacy exposure. - Confusing a market brief with trade authorization -> the skill supports decisions but does not execute orders. ## External Endpoints | Endpoint | Data Sent | Purpose | |----------|-----------|---------| | `https://finance.yahoo.com/quote/{symbol}` | ticker symbol in URL path | Current price panel and headline quote statistics | | `https://query1.finance.yahoo.com/v1/finance/search` | query text, quote/news count | Symbol lookup and related headline search | No other data is sent externally. ## Security & Privacy **Data that leaves your machine:** - Ticker symbols or text search queries sent to Yahoo Finance pages and search endpoints **Data that stays local when the user opts in:** - Watchlists, preferred benchmarks, briefing notes, and decision boundaries in `~/yahoo/` **This skill does NOT:** - Place broker orders or connect to brokerage accounts - Store credentials, account numbers, or tax documents - Create or update local files without user approval - Make undeclared network requests outside Yahoo Finance endpoints ## Trust By using this skill, ticker symbols and search queries are sent to Yahoo Finance. Only install if you trust Yahoo with that market lookup data. ## Data Storage All local state lives in `~/yahoo/` when the user wants continuity. Keep storage compact: watchlists, repeated benchmark sets, answer-style preferences, and thesis reviews. Avoid full holdings exports unless the user explicitly asks for local note-taking. ## Related Skills Install with `clawhub install <slug>` if user confirms: - `analysis` — turn raw market inputs into explicit conclusions and tradeoffs - `economics` — add macro context when rates, inflation, or policy events matter - `news` — expand beyond Yahoo headlines when the story needs cross-source verification - `personal-finance-tracker` — connect market decisions to cashflow and risk capacity - `trading` — structure execution checklists and post-trade reviews ## Feedback - If useful: `clawhub star yahoo` - Stay updated: `clawhub sync`

path: commands.md
content: # Yahoo Command Recipes

Run these from the installed skill folder.

## Symbol lookup

```bash
python3 yahoo_search.py "apple"
python3 yahoo_search.py "semiconductor etf"
python3 yahoo_search.py "bitcoin"
```

Use this first when the user gives a company name, theme, ETF nickname, or an ambiguous symbol.

## Single-symbol snapshot

```bash
python3 yahoo_quote.py AAPL
python3 yahoo_quote.py NVDA
python3 yahoo_quote.py BTC-USD
```

Use this for price, daily move, market state, and headline statistics on one instrument.

## Multi-ticker market brief

```bash
python3 yahoo_brief.py AAPL MSFT NVDA AMZN
python3 yahoo_brief.py SPY QQQ IWM DIA
python3 yahoo_brief.py EURUSD=X GBPUSD=X JPY=X
```

Use this to rank movers, compare watchlists, or prep a pre-market and post-market pass.

## Decision workflow

1. Resolve the exact symbol with `yahoo_search.py`.
2. Pull the latest snapshot with `yahoo_quote.py`.
3. Run `yahoo_brief.py` for peers, benchmarks, or a full watchlist.
4. Frame the setup with `thesis-card.md`.
5. Apply `risk-playbook.md` before suggesting urgency or size.


path: market-playbook.md
content: # Yahoo Market Playbook

Use this when the user wants more than a raw quote.

## 1. Name the job

- Quick tape read
- Pre-market brief
- Post-market review
- Swing setup review
- Event watch
- No-trade monitoring

## 2. Anchor the comparison set

Always define what the symbol should be compared against:
- broad index
- sector ETF
- direct peers
- crypto benchmark
- currency basket

If there is no comparison set, say so explicitly.

## 3. State what Yahoo is showing now

Summarize:
- current price
- daily move
- market state or timestamp
- 52-week context
- volume or liquidity clue

Keep this separate from interpretation.

## 4. Map the nearest catalyst

Look for the next window that could matter:
- earnings
- macro releases
- guidance updates
- product or regulatory events
- sector rotation

If the setup has no obvious catalyst, mark it as watchlist-only unless the user wants a purely technical read.

## 5. Convert to a decision frame

End with one of these:
- trade candidate
- watchlist candidate
- needs more confirmation
- no-trade

Every decision frame should include why now, what would confirm it, and what would break it.


path: memory-template.md
content: # Memory Template - Yahoo

Create `~/yahoo/memory.md` with this structure:

```markdown
# Yahoo Memory

## Status
status: ongoing
version: 1.0.0
last: YYYY-MM-DD
integration: pending | done | declined

## Activation Preferences
- Trigger phrases:
- Use automatically or on request:

## Market Scope
- Instruments:
- Regions:
- Benchmarks:
- Exclusions:

## Watchlist Defaults
- Core watchlist:
- Priority names:
- Event names:

## Decision Boundaries
- Allowed depth:
- Risk limits:
- Things to avoid:

## Output Style
- Preferred format:
- Target length:
- When to ask a follow-up:

## Review Notes
- Repeated thesis patterns worth remembering
- Mistakes to avoid in future briefings

## Notes
- Short context that improves the next Yahoo workflow

---
*Updated: YYYY-MM-DD*
```

## Status Values

| Value | Meaning | Behavior |
|-------|---------|----------|
| `ongoing` | Context is still evolving | Ask only when a missing detail changes the market answer materially |
| `complete` | Stable Yahoo workflow exists | Prioritize execution and targeted updates |
| `paused` | User wants no new setup questions | Keep existing memory and answer directly |
| `never_ask` | User does not want setup prompts | Do not ask activation questions unless requested |

## Writing Rules

- Keep entries short and operational.
- Save durable watchlist and decision context, not noisy one-off tape moves.
- Update `last` after meaningful Yahoo sessions.
- Prefer natural language notes over config-like key explosions.
- Never persist credentials, exact account holdings, or sensitive tax material by default.


path: risk-playbook.md
content: # Yahoo Risk Playbook

Use this before suggesting urgency, conviction, or size.

## Pre-decision checklist

- Is the market state clear: live, at close, pre-market, or after-hours?
- Is the instrument liquid enough for the user's horizon?
- Is a catalyst near enough to make today's print misleading?
- Is the user asking for data only, or for a trade-style opinion?
- Is there an explicit invalidation, or is the setup still just narrative?

## Red flags

- Very thin volume
- Extreme move with no clear catalyst
- Symbol ambiguity
- User has no stated horizon
- User is asking for certainty instead of scenarios

## Output rules

- If risk is unclear, downgrade to watchlist or needs more confirmation.
- If the user wants a trade-style answer, include invalidation and what evidence would reverse the call.
- If the data is stale or outside regular hours, say that first.
- Never imply order execution or account access.


path: setup.md
content: # Setup - Yahoo

Use this file when `~/yahoo/` is missing or empty, or when the user wants Yahoo Finance behavior to persist in the current workspace.

Keep setup short. Answer the market question first, then install only the minimum continuity needed for better future briefs.

## Immediate First-Run Actions

### 1. Offer local continuity only if it helps

Do not create finance notes by default.

If the user wants persistence, create the local folder and baseline files:

```bash
mkdir -p ~/yahoo
touch ~/yahoo/memory.md ~/yahoo/watchlist.md ~/yahoo/briefing-log.md ~/yahoo/decisions.md
chmod 700 ~/yahoo
chmod 600 ~/yahoo/memory.md ~/yahoo/watchlist.md ~/yahoo/briefing-log.md ~/yahoo/decisions.md
```

If `~/yahoo/memory.md` is empty, initialize it from `memory-template.md`.

### 2. Prepare workspace routing non-destructively

If a workspace `AGENTS.md` exists, propose a minimal routing snippet and wait for explicit approval before writing it:

```markdown
## Yahoo Routing

When the user mentions Yahoo Finance, stock quotes, ticker lookups, watchlists, earnings dates, pre-market moves, or market briefs:
- activate the installed `yahoo` skill
- read `~/yahoo/memory.md` if it exists
- use `yahoo_search.py` for ambiguous names before analysis
- use `yahoo_quote.py` for single-name tape reads and `yahoo_brief.py` for baskets
- load `market-playbook.md`, `thesis-card.md`, and `risk-playbook.md` when the task moves from data to decision
```

If the workspace already routes market or trading work, refine the smallest relevant block instead of duplicating it.

### 3. Personalize lightly while helping

Do not run a questionnaire.

In the first exchanges, establish only what changes the output materially:
- activation preference: always on for Yahoo Finance requests or direct request only
- market scope: US equities only, ETFs too, crypto pairs too, or mixed
- answer style: quick tape read, concise brief, or deeper thesis review
- risk depth: data only, setup plus invalidation, or full trade/no-trade framing

### 4. What to save

- preferred benchmark set or recurring watchlist
- answer style and detail level
- repeated risk boundaries such as no options, no leverage, or no microcaps
- durable sector focus or strategy focus
- thesis review notes that improve future decision quality

Do not save broker credentials, precise holdings, tax records, or one-off searches that will not matter later.

## Setup Completion

Setup is sufficient when:
- the user has their answer for the current market question
- activation preference is clear or intentionally skipped
- `~/yahoo/memory.md` exists only if the user wanted continuity
- the next Yahoo Finance request can start faster than the first one


path: thesis-card.md
content: # Yahoo Thesis Card

Use this template when the user wants a decision-ready view instead of loose commentary.

```markdown
# {Symbol} Thesis Card

## Objective
- Horizon:
- Why this name matters now:

## Current Yahoo Snapshot
- Price:
- Daily move:
- Market state:
- Key statistics:

## Catalyst
- Nearest catalyst window:
- Why it matters:

## Thesis
- Bull case:
- Bear case:
- What must be true:

## Trigger
- Entry condition:
- Confirmation signs:

## Invalidation
- Level or condition that breaks the setup:
- What to do if invalidated:

## Verdict
- Trade candidate | Watchlist | Needs more confirmation | No-trade
- Confidence:
```

Keep the card short enough that a future review can compare outcome versus plan.


path: yahoo_brief.py
content: #!/usr/bin/env python3
"""Create a compact multi-ticker Yahoo Finance market brief."""

from __future__ import annotations

import argparse
import gzip
import html
import re
import statistics
import sys
from dataclasses import dataclass
from pathlib import Path
from urllib.request import Request, urlopen


HEADERS = {
    "User-Agent": "Mozilla/5.0",
    "Accept-Encoding": "identity",
}


@dataclass
class Snapshot:
    symbol: str
    title: str
    price: str
    change: str
    change_pct_value: float
    change_pct_text: str
    market_time: str


def fetch_html(symbol: str) -> str:
    req = Request(f"https://finance.yahoo.com/quote/{symbol}", headers=HEADERS)
    with urlopen(req, timeout=20) as response:
        payload = response.read()
        encoding = (response.info().get("Content-Encoding") or "").lower()
    if encoding == "gzip" or payload[:2] == b"\x1f\x8b":
        payload = gzip.decompress(payload)
    return payload.decode("utf-8", "replace")


def extract(pattern: str, text: str) -> str:
    match = re.search(pattern, text, re.S)
    if not match:
        return ""
    return " ".join(html.unescape(match.group(1)).split())


def parse_percent(raw: str) -> float:
    cleaned = raw.replace("(", "").replace(")", "").replace("%", "").replace("+", "").replace(",", "").strip()
    return float(cleaned or 0)


def parse_title(page: str, fallback_symbol: str) -> str:
    title = extract(r"<title>(.*?)</title>", page)
    title = title.replace(" - Yahoo Finance", "").replace("Stock Price, News, Quote & History", "").strip(" |")
    return title or fallback_symbol.upper()


def load_symbols(args_symbols: list[str], file_path: str | None) -> list[str]:
    symbols = [item.upper() for item in args_symbols]
    if file_path:
        path = Path(file_path).expanduser()
        for line in path.read_text(encoding="utf-8").splitlines():
            line = line.strip().upper()
            if line and not line.startswith("#"):
                symbols.append(line)
    seen = set()
    unique = []
    for symbol in symbols:
        if symbol not in seen:
            seen.add(symbol)
            unique.append(symbol)
    return unique


def fetch_snapshot(symbol: str) -> Snapshot:
    page = fetch_html(symbol)
    change_pct_text = extract(r'data-testid="qsp-price-change-percent">\s*([^<]+)', page) or "(0.00%)"
    return Snapshot(
        symbol=symbol,
        title=parse_title(page, symbol),
        price=extract(r'data-testid="qsp-price">\s*([^<]+)', page) or "?",
        change=extract(r'data-testid="qsp-price-change">\s*([^<]+)', page) or "?",
        change_pct_value=parse_percent(change_pct_text),
        change_pct_text=change_pct_text,
        market_time=extract(r'<div slot="marketTimeNotice"[^>]*>\s*<span[^>]*>([^<]+)', page),
    )


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("symbols", nargs="*", help="tickers to include")
    parser.add_argument("--file", help="optional file with one symbol per line")
    args = parser.parse_args()

    symbols = load_symbols(args.symbols, args.file)
    if not symbols:
        print("Provide at least one symbol or --file.", file=sys.stderr)
        return 1

    snapshots: list[Snapshot] = []
    failures: list[str] = []
    for symbol in symbols:
        try:
            snapshots.append(fetch_snapshot(symbol))
        except Exception as exc:
            failures.append(f"{symbol}: {exc}")

    if not snapshots:
        print("No quotes could be fetched.", file=sys.stderr)
        for failure in failures:
            print(failure, file=sys.stderr)
        return 1

    movers = sorted(snapshots, key=lambda item: item.change_pct_value, reverse=True)
    avg_move = statistics.mean(item.change_pct_value for item in snapshots)

    print("Yahoo Market Brief")
    if snapshots[0].market_time:
        print(f"Market state: {snapshots[0].market_time}")
    print(f"Universe: {', '.join(item.symbol for item in snapshots)}")
    print(f"Average move: {avg_move:+.2f}%")
    print()

    print("Ranked movers")
    for item in movers:
        print(f"- {item.symbol}: {item.price} | {item.change} {item.change_pct_text} | {item.title}")

    print()
    print("Leaders")
    for item in movers[: min(3, len(movers))]:
        print(f"- {item.symbol}: {item.change_pct_text}")

    print()
    print("Laggards")
    for item in list(reversed(movers[-min(3, len(movers)) :])):
        print(f"- {item.symbol}: {item.change_pct_text}")

    if failures:
        print()
        print("Failures")
        for failure in failures:
            print(f"- {failure}")

    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: yahoo_quote.py
content: #!/usr/bin/env python3
"""Fetch a Yahoo Finance quote page and print key quote statistics."""

from __future__ import annotations

import argparse
import gzip
import html
import re
import sys
from urllib.request import Request, urlopen


HEADERS = {
    "User-Agent": "Mozilla/5.0",
    "Accept-Encoding": "identity",
}


def fetch_html(symbol: str) -> str:
    req = Request(f"https://finance.yahoo.com/quote/{symbol}", headers=HEADERS)
    with urlopen(req, timeout=20) as response:
        payload = response.read()
        encoding = (response.info().get("Content-Encoding") or "").lower()
    if encoding == "gzip" or payload[:2] == b"\x1f\x8b":
        payload = gzip.decompress(payload)
    return payload.decode("utf-8", "replace")


def extract(pattern: str, text: str) -> str:
    match = re.search(pattern, text, re.S)
    if not match:
        return ""
    return " ".join(html.unescape(match.group(1)).split())


def strip_tags(raw: str) -> str:
    text = re.sub(r"<[^>]+>", "", raw)
    return " ".join(html.unescape(text).split())


def parse_stats(page: str) -> list[tuple[str, str]]:
    block = re.search(r'<div data-testid="quote-statistics".*?<ul[^>]*>(.*?)</ul>', page, re.S)
    if not block:
        return []
    items = re.findall(
        r'<li[^>]*>\s*<span class="label[^"]*"[^>]*title="([^"]+)"[^>]*>.*?</span>\s*<span class="value[^"]*">(.*?)</span>\s*</li>',
        block.group(1),
        re.S,
    )
    return [(label, strip_tags(value)) for label, value in items]


def parse_title(page: str, fallback_symbol: str) -> str:
    title = extract(r"<title>(.*?)</title>", page)
    title = title.replace(" - Yahoo Finance", "").replace("Stock Price, News, Quote & History", "").strip(" |")
    if title and title != "Yahoo Finance":
        return title
    return fallback_symbol.upper()


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("symbol", help="ticker or Yahoo symbol, e.g. AAPL or BTC-USD")
    args = parser.parse_args()

    try:
        page = fetch_html(args.symbol.upper())
    except Exception as exc:
        print(f"Yahoo quote fetch failed: {exc}", file=sys.stderr)
        return 1

    title = parse_title(page, args.symbol)
    price = extract(r'data-testid="qsp-price">\s*([^<]+)', page)
    change = extract(r'data-testid="qsp-price-change">\s*([^<]+)', page)
    change_pct = extract(r'data-testid="qsp-price-change-percent">\s*([^<]+)', page)
    market_time = extract(r'<div slot="marketTimeNotice"[^>]*>\s*<span[^>]*>([^<]+)', page)
    stats = parse_stats(page)

    print(title)
    if price:
        print(f"Price: {price}")
    if change or change_pct:
        print(f"Move: {change} {change_pct}".strip())
    if market_time:
        print(f"Market state: {market_time}")

    if stats:
        print()
        print("Key statistics")
        for label, value in stats[:10]:
            print(f"- {label}: {value}")
    else:
        print()
        print("No statistics parsed from the Yahoo quote page.")

    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: yahoo_search.py
content: #!/usr/bin/env python3
"""Resolve Yahoo Finance symbols and show related headlines."""

from __future__ import annotations

import argparse
import json
import sys
from typing import Any
from urllib.parse import urlencode
from urllib.request import Request, urlopen


SEARCH_URL = "https://query1.finance.yahoo.com/v1/finance/search"
HEADERS = {
    "User-Agent": "Mozilla/5.0",
    "Accept": "application/json",
    "Accept-Encoding": "identity",
}


def fetch_json(query: str, quotes: int, news: int) -> dict[str, Any]:
    params = urlencode(
        {
            "q": query,
            "quotesCount": quotes,
            "newsCount": news,
            "enableFuzzyQuery": "false",
            "enableCb": "false",
        }
    )
    req = Request(f"{SEARCH_URL}?{params}", headers=HEADERS)
    with urlopen(req, timeout=20) as response:
        return json.loads(response.read().decode("utf-8"))


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("query", help="company name, ticker, ETF, or theme")
    parser.add_argument("--quotes", type=int, default=5, help="max quote matches")
    parser.add_argument("--news", type=int, default=3, help="max news items")
    args = parser.parse_args()

    try:
        payload = fetch_json(args.query, args.quotes, args.news)
    except Exception as exc:
        print(f"Yahoo search failed: {exc}", file=sys.stderr)
        return 1

    quotes = payload.get("quotes", [])
    news = payload.get("news", [])

    print(f"Yahoo Search: {args.query}")
    print()

    if quotes:
        print("Symbol matches")
        for item in quotes[: args.quotes]:
            name = item.get("shortname") or item.get("longname") or "(no name)"
            symbol = item.get("symbol", "?")
            kind = item.get("typeDisp") or item.get("quoteType") or "unknown"
            exch = item.get("exchDisp") or item.get("exchange") or "unknown"
            sector = item.get("sectorDisp") or item.get("sector") or ""
            sector_text = f" | {sector}" if sector else ""
            print(f"- {symbol}: {name} [{kind} | {exch}{sector_text}]")
    else:
        print("No symbol matches returned.")

    if news:
        print()
        print("Related headlines")
        for item in news[: args.news]:
            title = item.get("title") or "(untitled)"
            publisher = item.get("publisher") or item.get("provider") or "unknown"
            link = item.get("link") or ""
            print(f"- {title} [{publisher}]")
            if link:
                print(f"  {link}")

    return 0


if __name__ == "__main__":
    raise SystemExit(main())


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)